Create QUICHE toy CONNECT proxy server
PiperOrigin-RevId: 466743774
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 313dea4..90dd052 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -693,6 +693,7 @@
"common/platform/api/quiche_file_utils.h",
"common/platform/api/quiche_system_event_loop.h",
"quic/platform/api/quic_default_proof_providers.h",
+ "quic/tools/connect_server_backend.h",
"quic/tools/connect_tunnel.h",
"quic/tools/fake_proof_verifier.h",
"quic/tools/quic_backend_response.h",
@@ -717,6 +718,7 @@
]
quiche_tool_support_srcs = [
"common/platform/api/quiche_file_utils.cc",
+ "quic/tools/connect_server_backend.cc",
"quic/tools/connect_tunnel.cc",
"quic/tools/quic_backend_response.cc",
"quic/tools/quic_client_base.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index 0c08a73..842512d 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -693,6 +693,7 @@
"src/quiche/common/platform/api/quiche_file_utils.h",
"src/quiche/common/platform/api/quiche_system_event_loop.h",
"src/quiche/quic/platform/api/quic_default_proof_providers.h",
+ "src/quiche/quic/tools/connect_server_backend.h",
"src/quiche/quic/tools/connect_tunnel.h",
"src/quiche/quic/tools/fake_proof_verifier.h",
"src/quiche/quic/tools/quic_backend_response.h",
@@ -717,6 +718,7 @@
]
quiche_tool_support_srcs = [
"src/quiche/common/platform/api/quiche_file_utils.cc",
+ "src/quiche/quic/tools/connect_server_backend.cc",
"src/quiche/quic/tools/connect_tunnel.cc",
"src/quiche/quic/tools/quic_backend_response.cc",
"src/quiche/quic/tools/quic_client_base.cc",
diff --git a/build/source_list.json b/build/source_list.json
index a7fdf5b..6d14856 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -692,6 +692,7 @@
"quiche/common/platform/api/quiche_file_utils.h",
"quiche/common/platform/api/quiche_system_event_loop.h",
"quiche/quic/platform/api/quic_default_proof_providers.h",
+ "quiche/quic/tools/connect_server_backend.h",
"quiche/quic/tools/connect_tunnel.h",
"quiche/quic/tools/fake_proof_verifier.h",
"quiche/quic/tools/quic_backend_response.h",
@@ -716,6 +717,7 @@
],
"quiche_tool_support_srcs": [
"quiche/common/platform/api/quiche_file_utils.cc",
+ "quiche/quic/tools/connect_server_backend.cc",
"quiche/quic/tools/connect_tunnel.cc",
"quiche/quic/tools/quic_backend_response.cc",
"quiche/quic/tools/quic_client_base.cc",
diff --git a/quiche/quic/tools/connect_server_backend.cc b/quiche/quic/tools/connect_server_backend.cc
new file mode 100644
index 0000000..baee9c1
--- /dev/null
+++ b/quiche/quic/tools/connect_server_backend.cc
@@ -0,0 +1,133 @@
+// 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_server_backend.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/container/flat_hash_set.h"
+#include "absl/strings/string_view.h"
+#include "quiche/quic/core/io/socket_factory.h"
+#include "quiche/quic/tools/connect_tunnel.h"
+#include "quiche/quic/tools/quic_simple_server_backend.h"
+#include "quiche/common/platform/api/quiche_bug_tracker.h"
+#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/spdy/core/http2_header_block.h"
+
+namespace quic {
+
+namespace {
+
+void SendErrorResponse(QuicSimpleServerBackend::RequestHandler* request_handler,
+ absl::string_view error_code) {
+ spdy::Http2HeaderBlock headers;
+ headers[":status"] = error_code;
+ QuicBackendResponse response;
+ response.set_headers(std::move(headers));
+ request_handler->OnResponseBackendComplete(&response);
+}
+
+} // namespace
+
+ConnectServerBackend::ConnectServerBackend(
+ std::unique_ptr<QuicSimpleServerBackend> non_connect_backend,
+ absl::flat_hash_set<ConnectTunnel::HostAndPort> acceptable_destinations)
+ : non_connect_backend_(std::move(non_connect_backend)),
+ acceptable_destinations_(std::move(acceptable_destinations)) {
+ QUICHE_DCHECK(non_connect_backend_);
+}
+
+ConnectServerBackend::~ConnectServerBackend() {
+ // Expect all streams to be closed before destroying backend.
+ QUICHE_DCHECK(tunnels_.empty());
+}
+
+bool ConnectServerBackend::InitializeBackend(const std::string&) {
+ return true;
+}
+
+bool ConnectServerBackend::IsBackendInitialized() const { return true; }
+
+void ConnectServerBackend::SetSocketFactory(SocketFactory* socket_factory) {
+ QUICHE_DCHECK_NE(socket_factory_, socket_factory);
+ QUICHE_DCHECK(tunnels_.empty());
+ socket_factory_ = socket_factory;
+}
+
+void ConnectServerBackend::FetchResponseFromBackend(
+ const spdy::Http2HeaderBlock& request_headers,
+ const std::string& request_body, RequestHandler* request_handler) {
+ // Not a CONNECT request, so send to `non_connect_backend_`.
+ non_connect_backend_->FetchResponseFromBackend(request_headers, request_body,
+ request_handler);
+}
+
+void ConnectServerBackend::HandleConnectHeaders(
+ const spdy::Http2HeaderBlock& request_headers,
+ RequestHandler* request_handler) {
+ QUICHE_DCHECK(request_headers.contains(":method") &&
+ request_headers.find(":method")->second == "CONNECT");
+
+ if (!socket_factory_) {
+ QUICHE_BUG(connect_server_backend_no_socket_factory)
+ << "Must set socket factory before ConnectServerBackend receives "
+ "requests.";
+ SendErrorResponse(request_handler, "500");
+ return;
+ }
+
+ if (request_headers.contains(":protocol")) {
+ // Anything other than normal CONNECT not supported.
+ // TODO(ericorth): Add CONNECT-UDP support.
+ non_connect_backend_->HandleConnectHeaders(request_headers,
+ request_handler);
+ return;
+ }
+
+ auto [tunnel_it, inserted] = tunnels_.emplace(
+ request_handler->stream_id(),
+ std::make_unique<ConnectTunnel>(request_handler, socket_factory_,
+ acceptable_destinations_));
+ QUICHE_DCHECK(inserted);
+
+ tunnel_it->second->OpenTunnel(request_headers);
+}
+
+void ConnectServerBackend::HandleConnectData(absl::string_view data,
+ bool data_complete,
+ RequestHandler* request_handler) {
+ auto tunnel_it = tunnels_.find(request_handler->stream_id());
+ if (tunnel_it == tunnels_.end()) {
+ // If tunnel not found, perhaps it's something being handled for
+ // non-CONNECT. Possible because this method could be called for anything
+ // with a ":method":"CONNECT" header, but this class does not handle such
+ // requests if they have a ":protocol" header.
+ non_connect_backend_->HandleConnectData(data, data_complete,
+ request_handler);
+ return;
+ }
+
+ if (!data.empty()) {
+ tunnel_it->second->SendDataToDestination(data);
+ }
+ if (data_complete) {
+ tunnel_it->second->OnClientStreamClose();
+ tunnels_.erase(tunnel_it);
+ }
+}
+
+void ConnectServerBackend::CloseBackendResponseStream(
+ QuicSimpleServerBackend::RequestHandler* request_handler) {
+ auto tunnel_it = tunnels_.find(request_handler->stream_id());
+ if (tunnel_it != tunnels_.end()) {
+ tunnel_it->second->OnClientStreamClose();
+ tunnels_.erase(tunnel_it);
+ }
+
+ non_connect_backend_->CloseBackendResponseStream(request_handler);
+}
+
+} // namespace quic
diff --git a/quiche/quic/tools/connect_server_backend.h b/quiche/quic/tools/connect_server_backend.h
new file mode 100644
index 0000000..a1cd843
--- /dev/null
+++ b/quiche/quic/tools/connect_server_backend.h
@@ -0,0 +1,60 @@
+// 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.
+
+#ifndef QUICHE_QUIC_CONNECT_PROXY_CONNECT_SERVER_BACKEND_H_
+#define QUICHE_QUIC_CONNECT_PROXY_CONNECT_SERVER_BACKEND_H_
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/container/flat_hash_set.h"
+#include "quiche/quic/core/io/socket_factory.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/quic/tools/connect_tunnel.h"
+#include "quiche/quic/tools/quic_simple_server_backend.h"
+
+namespace quic {
+
+// QUIC server backend that handles CONNECT requests. Non-CONNECT requests are
+// delegated to a separate backend.
+class ConnectServerBackend : public QuicSimpleServerBackend {
+ public:
+ ConnectServerBackend(
+ std::unique_ptr<QuicSimpleServerBackend> non_connect_backend,
+ absl::flat_hash_set<ConnectTunnel::HostAndPort> acceptable_destinations);
+
+ ConnectServerBackend(const ConnectServerBackend&) = delete;
+ ConnectServerBackend& operator=(const ConnectServerBackend&) = delete;
+
+ ~ConnectServerBackend() override;
+
+ // QuicSimpleServerBackend:
+ bool InitializeBackend(const std::string& backend_url) override;
+ bool IsBackendInitialized() const override;
+ void SetSocketFactory(SocketFactory* socket_factory) override;
+ void FetchResponseFromBackend(const spdy::Http2HeaderBlock& request_headers,
+ const std::string& request_body,
+ RequestHandler* request_handler) override;
+ void HandleConnectHeaders(const spdy::Http2HeaderBlock& request_headers,
+ RequestHandler* request_handler) override;
+ void HandleConnectData(absl::string_view data, bool data_complete,
+ RequestHandler* request_handler) override;
+ void CloseBackendResponseStream(
+ QuicSimpleServerBackend::RequestHandler* request_handler) override;
+
+ private:
+ std::unique_ptr<QuicSimpleServerBackend> non_connect_backend_;
+ const absl::flat_hash_set<ConnectTunnel::HostAndPort>
+ acceptable_destinations_;
+
+ SocketFactory* socket_factory_; // unowned
+ absl::flat_hash_map<QuicStreamId, std::unique_ptr<ConnectTunnel>> tunnels_;
+};
+
+} // namespace quic
+
+#endif // QUICHE_QUIC_CONNECT_PROXY_CONNECT_SERVER_BACKEND_H_
diff --git a/quiche/quic/tools/connect_tunnel.cc b/quiche/quic/tools/connect_tunnel.cc
index bd0b66e..ef2a5eb 100644
--- a/quiche/quic/tools/connect_tunnel.cc
+++ b/quiche/quic/tools/connect_tunnel.cc
@@ -81,8 +81,8 @@
}
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));
+ return ConnectTunnel::HostAndPort(std::move(hostname),
+ static_cast<uint16_t>(parsed_port_number));
}
absl::optional<ConnectTunnel::HostAndPort> ValidateHeadersAndGetAuthority(
@@ -114,22 +114,28 @@
return ValidateAndParseAuthorityString(authority_it->second);
}
-bool ValidateAuthority(
- const ConnectTunnel::HostAndPort& authority,
- const absl::flat_hash_set<std::pair<std::string, uint16_t>>&
- acceptable_destinations) {
+bool ValidateAuthority(const ConnectTunnel::HostAndPort& authority,
+ const absl::flat_hash_set<ConnectTunnel::HostAndPort>&
+ acceptable_destinations) {
if (acceptable_destinations.contains(authority)) {
return true;
}
QUICHE_DVLOG(1) << "CONNECT request authority: "
- << absl::StrCat(authority.first, ":", authority.second)
+ << absl::StrCat(authority.host, ":", authority.port)
<< " is not an acceptable allow-listed destiation ";
return false;
}
} // namespace
+ConnectTunnel::HostAndPort::HostAndPort(std::string host, uint16_t port)
+ : host(std::move(host)), port(port) {}
+
+bool ConnectTunnel::HostAndPort::operator==(const HostAndPort& other) const {
+ return host == other.host && port == other.port;
+}
+
ConnectTunnel::ConnectTunnel(
QuicSimpleServerBackend::RequestHandler* client_stream_request_handler,
SocketFactory* socket_factory,
@@ -168,9 +174,8 @@
return;
}
- QuicSocketAddress address =
- tools::LookupAddress(AF_UNSPEC, authority.value().first,
- absl::StrCat(authority.value().second));
+ QuicSocketAddress address = tools::LookupAddress(
+ AF_UNSPEC, authority->host, absl::StrCat(authority->port));
if (!address.IsInitialized()) {
TerminateClientStream("host resolution error");
return;
@@ -193,7 +198,7 @@
QUICHE_DVLOG(1) << "CONNECT tunnel opened from stream "
<< client_stream_request_handler_->stream_id() << " to "
- << authority.value().first << ":" << authority.value().second;
+ << authority->host << ":" << authority->port;
SendConnectResponse();
BeginAsyncReadFromDestination();
diff --git a/quiche/quic/tools/connect_tunnel.h b/quiche/quic/tools/connect_tunnel.h
index f638324..d18d63f 100644
--- a/quiche/quic/tools/connect_tunnel.h
+++ b/quiche/quic/tools/connect_tunnel.h
@@ -26,7 +26,19 @@
// Manages a single connection tunneled over a CONNECT proxy.
class ConnectTunnel : public StreamClientSocket::AsyncVisitor {
public:
- using HostAndPort = std::pair<std::string, uint16_t>;
+ struct HostAndPort {
+ HostAndPort(std::string host, uint16_t port);
+
+ bool operator==(const HostAndPort& other) const;
+
+ template <typename H>
+ friend H AbslHashValue(H h, const HostAndPort& host_and_port) {
+ return H::combine(std::move(h), host_and_port.host, host_and_port.port);
+ }
+
+ std::string host;
+ uint16_t port;
+ };
// `client_stream_request_handler` and `socket_factory` must both outlive the
// created ConnectTunnel.
diff --git a/quiche/quic/tools/quic_server.cc b/quiche/quic/tools/quic_server.cc
index 53985e7..0c3233a 100644
--- a/quiche/quic/tools/quic_server.cc
+++ b/quiche/quic/tools/quic_server.cc
@@ -16,6 +16,7 @@
#include "quiche/quic/core/crypto/crypto_handshake.h"
#include "quiche/quic/core/crypto/quic_random.h"
+#include "quiche/quic/core/io/event_loop_socket_factory.h"
#include "quiche/quic/core/io/quic_default_event_loop.h"
#include "quiche/quic/core/io/quic_event_loop.h"
#include "quiche/quic/core/quic_clock.h"
@@ -32,6 +33,7 @@
#include "quiche/quic/tools/quic_simple_crypto_server_stream_helper.h"
#include "quiche/quic/tools/quic_simple_dispatcher.h"
#include "quiche/quic/tools/quic_simple_server_backend.h"
+#include "quiche/common/simple_buffer_allocator.h"
namespace quic {
@@ -103,11 +105,20 @@
QuicServer::~QuicServer() {
close(fd_);
fd_ = -1;
+
+ // Should be fine without because nothing should send requests to the backend
+ // after `this` is destroyed, but for extra pointer safety, clear the socket
+ // factory from the backend before the socket factory is destroyed.
+ quic_simple_server_backend_->SetSocketFactory(nullptr);
}
bool QuicServer::CreateUDPSocketAndListen(const QuicSocketAddress& address) {
event_loop_ = CreateEventLoop();
+ socket_factory_ = std::make_unique<EventLoopSocketFactory>(
+ event_loop_.get(), quiche::SimpleBufferAllocator::Get());
+ quic_simple_server_backend_->SetSocketFactory(socket_factory_.get());
+
QuicUdpSocketApi socket_api;
fd_ = socket_api.Create(address.host().AddressFamilyToInt(),
/*receive_buffer_size =*/kDefaultSocketReceiveBuffer,
diff --git a/quiche/quic/tools/quic_server.h b/quiche/quic/tools/quic_server.h
index 7cb870f..d080cb1 100644
--- a/quiche/quic/tools/quic_server.h
+++ b/quiche/quic/tools/quic_server.h
@@ -16,6 +16,7 @@
#include "absl/strings/string_view.h"
#include "quiche/quic/core/crypto/quic_crypto_server_config.h"
#include "quiche/quic/core/io/quic_event_loop.h"
+#include "quiche/quic/core/io/socket_factory.h"
#include "quiche/quic/core/quic_config.h"
#include "quiche/quic/core/quic_packet_writer.h"
#include "quiche/quic/core/quic_udp_socket.h"
@@ -114,6 +115,9 @@
// Schedules alarms and notifies the server of the I/O events.
std::unique_ptr<QuicEventLoop> event_loop_;
+ // Used by some backends to create additional sockets, e.g. for upstream
+ // destination connections for proxying.
+ std::unique_ptr<SocketFactory> socket_factory_;
// Accepts data from the framer and demuxes clients to sessions.
std::unique_ptr<QuicDispatcher> dispatcher_;
diff --git a/quiche/quic/tools/quic_simple_server_backend.h b/quiche/quic/tools/quic_simple_server_backend.h
index bbb0d93..26eaa65 100644
--- a/quiche/quic/tools/quic_simple_server_backend.h
+++ b/quiche/quic/tools/quic_simple_server_backend.h
@@ -9,10 +9,10 @@
#include <memory>
#include "absl/strings/string_view.h"
+#include "quiche/quic/core/io/socket_factory.h"
#include "quiche/quic/core/quic_error_codes.h"
#include "quiche/quic/core/quic_types.h"
#include "quiche/quic/core/web_transport_interface.h"
-#include "quiche/quic/platform/api/quic_logging.h"
#include "quiche/quic/tools/quic_backend_response.h"
#include "quiche/spdy/core/http2_header_block.h"
@@ -59,6 +59,10 @@
// Returns true if the backend has been successfully initialized
// and could be used to fetch HTTP requests
virtual bool IsBackendInitialized() const = 0;
+ // Passes the socket factory in use by the QuicServer. Must live as long as
+ // incoming requests/data are still sent to the backend, or until cleared by
+ // calling with null. Must not be called while backend is handling requests.
+ virtual void SetSocketFactory(SocketFactory* /*socket_factory*/) {}
// Triggers a HTTP request to be sent to the backend server or cache
// If response is immediately available, the function synchronously calls
// the `request_handler` with the HTTP response.
diff --git a/quiche/quic/tools/quic_toy_server.cc b/quiche/quic/tools/quic_toy_server.cc
index 6b3c76f..500e5a1 100644
--- a/quiche/quic/tools/quic_toy_server.cc
+++ b/quiche/quic/tools/quic_toy_server.cc
@@ -4,14 +4,20 @@
#include "quiche/quic/tools/quic_toy_server.h"
+#include <limits>
#include <utility>
#include <vector>
+#include "absl/strings/str_split.h"
+#include "url/third_party/mozilla/url_parse.h"
#include "quiche/quic/core/quic_versions.h"
#include "quiche/quic/platform/api/quic_default_proof_providers.h"
#include "quiche/quic/platform/api/quic_socket_address.h"
+#include "quiche/quic/tools/connect_server_backend.h"
+#include "quiche/quic/tools/connect_tunnel.h"
#include "quiche/quic/tools/quic_memory_cache_backend.h"
#include "quiche/common/platform/api/quiche_command_line_flags.h"
+#include "quiche/common/platform/api/quiche_logging.h"
DEFINE_QUICHE_COMMAND_LINE_FLAG(int32_t, port, 6121,
"The port the quic server will listen on.");
@@ -39,8 +45,54 @@
DEFINE_QUICHE_COMMAND_LINE_FLAG(bool, enable_webtransport, false,
"If true, WebTransport support is enabled.");
+DEFINE_QUICHE_COMMAND_LINE_FLAG(
+ std::string, connect_proxy_destinations, "",
+ "Specifies a comma-separated list of destinations (\"hostname:port\") to "
+ "which the quic server will allow tunneling via CONNECT.");
+
namespace quic {
+namespace {
+
+ConnectTunnel::HostAndPort ParseProxyDestination(
+ absl::string_view destination) {
+ url::Component username_component;
+ url::Component password_component;
+ url::Component host_component;
+ url::Component port_component;
+
+ url::ParseAuthority(destination.data(), url::Component(0, destination.size()),
+ &username_component, &password_component, &host_component,
+ &port_component);
+
+ // Only support "host:port"
+ QUICHE_CHECK(!username_component.is_valid() &&
+ !password_component.is_valid());
+ QUICHE_CHECK(host_component.is_nonempty() && port_component.is_nonempty());
+
+ QUICHE_CHECK_LT(static_cast<size_t>(host_component.end()),
+ destination.size());
+ if (host_component.len > 2 && destination[host_component.begin] == '[' &&
+ destination[host_component.end() - 1] == ']') {
+ // Strip "[]" off IPv6 literals.
+ host_component.begin += 1;
+ host_component.len -= 2;
+ }
+ std::string hostname(destination.data() + host_component.begin,
+ host_component.len);
+
+ int parsed_port_number = url::ParsePort(destination.data(), port_component);
+
+ // Require specified and valid port.
+ QUICHE_CHECK_GT(parsed_port_number, 0);
+ QUICHE_CHECK_LE(parsed_port_number, std::numeric_limits<uint16_t>::max());
+
+ return ConnectTunnel::HostAndPort(std::move(hostname),
+ static_cast<uint16_t>(parsed_port_number));
+}
+
+} // namespace
+
std::unique_ptr<quic::QuicSimpleServerBackend>
QuicToyServer::MemoryCacheBackendFactory::CreateBackend() {
auto memory_cache_backend = std::make_unique<QuicMemoryCacheBackend>();
@@ -55,6 +107,21 @@
if (quiche::GetQuicheCommandLineFlag(FLAGS_enable_webtransport)) {
memory_cache_backend->EnableWebTransport();
}
+
+ if (!quiche::GetQuicheCommandLineFlag(FLAGS_connect_proxy_destinations)
+ .empty()) {
+ absl::flat_hash_set<ConnectTunnel::HostAndPort> connect_proxy_destinations;
+ for (absl::string_view destination : absl::StrSplit(
+ quiche::GetQuicheCommandLineFlag(FLAGS_connect_proxy_destinations),
+ ',', absl::SkipEmpty())) {
+ connect_proxy_destinations.insert(ParseProxyDestination(destination));
+ }
+ QUICHE_CHECK(!connect_proxy_destinations.empty());
+
+ return std::make_unique<ConnectServerBackend>(
+ std::move(memory_cache_backend), std::move(connect_proxy_destinations));
+ }
+
return memory_cache_backend;
}