diff --git a/api/envoy/extensions/transport_sockets/http_11_proxy/v3/upstream_http_11_connect.proto b/api/envoy/extensions/transport_sockets/http_11_proxy/v3/upstream_http_11_connect.proto index c0134c83374e7..375620c970ad2 100644 --- a/api/envoy/extensions/transport_sockets/http_11_proxy/v3/upstream_http_11_connect.proto +++ b/api/envoy/extensions/transport_sockets/http_11_proxy/v3/upstream_http_11_connect.proto @@ -36,6 +36,10 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // If the ``default_proxy_address`` is set and proxy address is not found in // ``typed_filter_metadata``, the default proxy address is used. // +// Optionally, the key ``envoy.http11_proxy_transport_socket.proxy_authorization`` and the +// proxy authorization value in ``google.protobuf.StringValue`` format can be set to send the +// ``Proxy-Authorization`` header with the ``CONNECT`` request. +// message Http11ProxyUpstreamTransport { // The underlying transport socket being wrapped. Defaults to plaintext (raw_buffer) if unset. config.core.v3.TransportSocket transport_socket = 1; diff --git a/source/common/config/well_known_names.h b/source/common/config/well_known_names.h index 9da41a35dcbd7..0264555aa0cee 100644 --- a/source/common/config/well_known_names.h +++ b/source/common/config/well_known_names.h @@ -40,6 +40,12 @@ class MetadataFilterValues { // Proxy address configuration namespace for HTTP/1.1 proxy transport sockets. const std::string ENVOY_HTTP11_PROXY_TRANSPORT_SOCKET_ADDR = "envoy.http11_proxy_transport_socket.proxy_address"; + + // Proxy-Authorization header value for HTTP/1.1 proxy transport sockets. + // When present, the value (a google.protobuf.StringValue) is added as a + // "Proxy-Authorization" header in the HTTP/1.1 CONNECT request. + const std::string ENVOY_HTTP11_PROXY_TRANSPORT_SOCKET_AUTH = + "envoy.http11_proxy_transport_socket.proxy_authorization"; }; using MetadataFilters = ConstSingleton; diff --git a/source/extensions/transport_sockets/http_11_proxy/connect.cc b/source/extensions/transport_sockets/http_11_proxy/connect.cc index 0c6d2ec82ddb4..f9333d99db8dd 100644 --- a/source/extensions/transport_sockets/http_11_proxy/connect.cc +++ b/source/extensions/transport_sockets/http_11_proxy/connect.cc @@ -10,6 +10,8 @@ #include "source/common/config/well_known_names.h" #include "source/common/http/header_utility.h" #include "source/common/network/address_impl.h" +#include "source/common/protobuf/protobuf.h" +#include "source/common/protobuf/utility.h" #include "source/common/runtime/runtime_features.h" namespace Envoy { @@ -69,8 +71,15 @@ UpstreamHttp11ConnectSocket::UpstreamHttp11ConnectSocket( } // Helper method to create a properly formatted CONNECT request with Host header. -std::string UpstreamHttp11ConnectSocket::formatConnectRequest(absl::string_view target) { - return absl::StrCat("CONNECT ", target, " HTTP/1.1\r\n", "Host: ", target, "\r\n\r\n"); +std::string UpstreamHttp11ConnectSocket::formatConnectRequest(absl::string_view target, + bool include_host_header, + absl::string_view authorization) { + std::string connect_header = absl::StrCat("CONNECT ", target, " HTTP/1.1\r\n"); + std::string host_header = include_host_header ? absl::StrCat("Host: ", target, "\r\n") : ""; + std::string proxy_authorization_header = + !authorization.empty() ? absl::StrCat("Proxy-Authorization: ", authorization, "\r\n") : ""; + + return absl::StrCat(connect_header, host_header, proxy_authorization_header, "\r\n"); } inline void UpstreamHttp11ConnectSocket::handleProxyInfoConnect( @@ -82,10 +91,10 @@ inline void UpstreamHttp11ConnectSocket::handleProxyInfoConnect( if (!Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.http_11_proxy_connect_legacy_format")) { // RFC 9110 compliant CONNECT format that includes Host header. - header_buffer_.add(formatConnectRequest(target)); + header_buffer_.add(formatConnectRequest(target, true /* include_host_header */)); } else { // Legacy behavior: no Host header for backward compatibility. - header_buffer_.add(absl::StrCat("CONNECT ", target, " HTTP/1.1\r\n\r\n")); + header_buffer_.add(formatConnectRequest(target, false /* include_host_header */)); } need_to_strip_connect_response_ = true; } @@ -93,6 +102,24 @@ inline void UpstreamHttp11ConnectSocket::handleProxyInfoConnect( inline void UpstreamHttp11ConnectSocket::handleHostMetadataConnect( std::shared_ptr host) { + // Look up the optional Proxy-Authorization value from the endpoint's typed metadata. + std::string authorization; + if (host->metadata() != nullptr) { + auto auth_it = host->metadata()->typed_filter_metadata().find( + Config::MetadataFilters::get().ENVOY_HTTP11_PROXY_TRANSPORT_SOCKET_AUTH); + if (auth_it != host->metadata()->typed_filter_metadata().end()) { + Protobuf::StringValue auth_value; + if (MessageUtil::unpackTo(auth_it->second, auth_value).ok()) { + authorization = auth_value.value(); + } else { + ENVOY_CONN_LOG(trace, + "Failed to unpack Proxy-Authorization string from host metadata, " + "proceeding with empty authorization", + callbacks_->connection()); + } + } + } + if (!Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.http_11_proxy_connect_legacy_format")) { // Prefer : for RFC 9110 compliance, unless URI is :. @@ -103,11 +130,11 @@ inline void UpstreamHttp11ConnectSocket::handleHostMetadataConnect( } else { target = host->address()->asStringView(); } - header_buffer_.add(formatConnectRequest(target)); + header_buffer_.add(formatConnectRequest(target, true /* include_host_header */, authorization)); } else { // Legacy behavior: : format, no Host header for backward compatibility. - header_buffer_.add( - absl::StrCat("CONNECT ", host->address()->asStringView(), " HTTP/1.1\r\n\r\n")); + header_buffer_.add(formatConnectRequest(host->address()->asStringView(), + false /* include_host_header */, authorization)); } need_to_strip_connect_response_ = true; } diff --git a/source/extensions/transport_sockets/http_11_proxy/connect.h b/source/extensions/transport_sockets/http_11_proxy/connect.h index b96c6740c1e72..4e18283decbc1 100644 --- a/source/extensions/transport_sockets/http_11_proxy/connect.h +++ b/source/extensions/transport_sockets/http_11_proxy/connect.h @@ -29,8 +29,12 @@ class UpstreamHttp11ConnectSocket : public TransportSockets::PassthroughSocket, // Helper method to create a properly formatted CONNECT request with Host header. // @param target the target hostname:port or IP:port to connect to. + // @param include_host_header whether to include the Host header in the CONNECT request. + // @param authorization when non-empty, added as the Proxy-Authorization header in the CONNECT + // request. // @return a properly formatted CONNECT request string per RFC 9110 section 9.3.6. - static std::string formatConnectRequest(absl::string_view target); + static std::string formatConnectRequest(absl::string_view target, bool include_host_header, + absl::string_view authorization = ""); UpstreamHttp11ConnectSocket( Network::TransportSocketPtr&& transport_socket, diff --git a/test/extensions/transport_sockets/http_11_proxy/connect_integration_test.cc b/test/extensions/transport_sockets/http_11_proxy/connect_integration_test.cc index cc7c0d9beeb8a..c7f53241a4f50 100644 --- a/test/extensions/transport_sockets/http_11_proxy/connect_integration_test.cc +++ b/test/extensions/transport_sockets/http_11_proxy/connect_integration_test.cc @@ -1,11 +1,14 @@ #include "envoy/config/common/key_value/v3/config.pb.h" +#include "envoy/config/core/v3/address.pb.h" #include "envoy/config/core/v3/base.pb.h" #include "envoy/config/core/v3/health_check.pb.h" #include "envoy/extensions/key_value/file_based/v3/config.pb.h" #include "envoy/extensions/transport_sockets/http_11_proxy/v3/upstream_http_11_connect.pb.h" #include "envoy/extensions/transport_sockets/raw_buffer/v3/raw_buffer.pb.h" +#include "source/common/config/well_known_names.h" #include "source/common/network/utility.h" +#include "source/common/protobuf/protobuf.h" #include "test/integration/http_integration.h" #include "test/integration/integration.h" @@ -98,7 +101,12 @@ name: envoy.clusters.dynamic_forward_proxy addFakeUpstream(upstreamProtocol()); } fake_upstreams_[1]->setDisableAllAndDoNotEnable(true); - default_proxy_address_ = fake_upstreams_[1]->localAddress(); + if (use_host_metadata_proxy_) { + // Populating default_proxy_address_ + host_metadata_proxy_address_ = fake_upstreams_[1]->localAddress(); + } else { + default_proxy_address_ = fake_upstreams_[1]->localAddress(); + } } config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { @@ -123,6 +131,30 @@ name: envoy.clusters.dynamic_forward_proxy auto* cluster = bootstrap.mutable_static_resources()->mutable_clusters(0); + if (use_host_metadata_proxy_) { + auto* lb_endpoint = + cluster->mutable_load_assignment()->mutable_endpoints(0)->mutable_lb_endpoints(0); + auto* md = lb_endpoint->mutable_metadata(); + + envoy::config::core::v3::Address addr_proto; + Network::Utility::addressToProtobufAddress(*host_metadata_proxy_address_, addr_proto); + Protobuf::Any addr_any; + std::ignore = addr_any.PackFrom(addr_proto); + (*md->mutable_typed_filter_metadata())[Config::MetadataFilters::get() + .ENVOY_HTTP11_PROXY_TRANSPORT_SOCKET_ADDR] = + addr_any; + + if (!host_metadata_proxy_authorization_.empty()) { + Protobuf::StringValue auth_value; + auth_value.set_value(host_metadata_proxy_authorization_); + Protobuf::Any auth_any; + std::ignore = auth_any.PackFrom(auth_value); + (*md->mutable_typed_filter_metadata())[Config::MetadataFilters::get() + .ENVOY_HTTP11_PROXY_TRANSPORT_SOCKET_AUTH] = + auth_any; + } + } + ConfigHelper::HttpProtocolOptions protocol_options; protocol_options.mutable_upstream_http_protocol_options()->set_auto_sni(true); protocol_options.mutable_upstream_http_protocol_options()->set_auto_san_validation(true); @@ -184,6 +216,9 @@ name: envoy.clusters.dynamic_forward_proxy bool pre_create_upstreams_ = false; Network::Address::InstanceConstSharedPtr default_proxy_address_; + bool use_host_metadata_proxy_ = false; + std::string host_metadata_proxy_authorization_; + Network::Address::InstanceConstSharedPtr host_metadata_proxy_address_; }; INSTANTIATE_TEST_SUITE_P(IpVersions, Http11ConnectHttpIntegrationTest, @@ -638,5 +673,37 @@ TEST_P(Http11ConnectHttpIntegrationTest, ConfiguredProxy) { ASSERT_FALSE(response->headers().get(Http::LowerCaseString("foo")).empty()); } +TEST_P(Http11ConnectHttpIntegrationTest, ProxyAuthorizationViaHostMetadata) { + pre_create_upstreams_ = true; + use_host_metadata_proxy_ = true; + host_metadata_proxy_authorization_ = "Basic abcdefghijk"; + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + + // Envoy dials the proxy (fake upstream 1) from the endpoint address metadata. + ASSERT_TRUE(fake_upstreams_[1]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + + // Verify the CONNECT request contains the Proxy-Authorization header. + std::string prefix_data; + ASSERT_TRUE(fake_upstream_connection_->waitForInexactRawData("\r\n\r\n", prefix_data)); + const std::string target = fake_upstreams_[0]->localAddress()->asString(); + const std::string expected_connect = + absl::StrCat("CONNECT ", target, " HTTP/1.1\r\n", "Host: ", target, "\r\n", + "Proxy-Authorization: ", host_metadata_proxy_authorization_, "\r\n\r\n"); + EXPECT_EQ(expected_connect, prefix_data); + + // Ship the CONNECT response and complete the encapsulated exchange. + fake_upstream_connection_->writeRawData("HTTP/1.1 200 OK\r\n\r\n"); + ASSERT_TRUE(fake_upstream_connection_->readDisable(false)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); + upstream_request_->encodeHeaders(default_response_headers_, true); + + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_EQ("200", response->headers().getStatusValue()); +} + } // namespace } // namespace Envoy diff --git a/test/extensions/transport_sockets/http_11_proxy/connect_test.cc b/test/extensions/transport_sockets/http_11_proxy/connect_test.cc index 712d71ba82185..614b72a948e3d 100644 --- a/test/extensions/transport_sockets/http_11_proxy/connect_test.cc +++ b/test/extensions/transport_sockets/http_11_proxy/connect_test.cc @@ -49,14 +49,16 @@ class Http11ConnectTest : public testing::TestWithParam target_port = {}) { initializeInternal(no_proxy_protocol, false, target_port); } // Initialize the test with the proxy address provided via endpoint metadata. - void initializeWithMetadataProxyAddr(bool with_hostname = false) { - initializeInternal(false, true, {}, with_hostname); + void initializeWithMetadataProxyAddr(bool with_hostname = false, + const std::string& auth_value = "") { + initializeInternal(false, true, {}, with_hostname, false, auth_value); } // Initialize the test with the proxy address provided via default proxy address. @@ -108,7 +110,7 @@ class Http11ConnectTest : public testing::TestWithParam target_port, bool with_hostname = false, - bool use_default_proxy_addr = false) { + bool use_default_proxy_addr = false, const std::string& auth_value = "") { std::string address_string = absl::StrCat(Network::Test::getLoopbackAddressUrlString(GetParam()), ":1234"); Network::Address::InstanceConstSharedPtr address = @@ -131,6 +133,16 @@ class Http11ConnectTest : public testing::TestWithParammutable_typed_filter_metadata()->emplace(std::make_pair(metadata_key, anypb)); + + if (!auth_value.empty()) { + const std::string auth_key = + Config::MetadataFilters::get().ENVOY_HTTP11_PROXY_TRANSPORT_SOCKET_AUTH; + Protobuf::StringValue auth_proto; + auth_proto.set_value(auth_value); + Protobuf::Any auth_anypb; + std::ignore = auth_anypb.PackFrom(auth_proto); + metadata->mutable_typed_filter_metadata()->emplace(auth_key, auth_anypb); + } EXPECT_CALL(*host, metadata()).Times(AnyNumber()).WillRepeatedly(Return(metadata)); if (with_hostname) { @@ -620,31 +632,100 @@ TEST(ParseTest, ContentLengthZeroHttp11) { EXPECT_FALSE(parser.parser().hasTransferEncoding()); } -// Test the formatConnectRequest() utility method with various inputs. -TEST(FormatConnectRequestTest, FormatConnectRequestWithVariousInputs) { +// Test the formatConnectRequest() utility method with various inputs, with the Host header +// included. +TEST(FormatConnectRequestTest, FormatConnectRequestWithHostHeader) { // Test with hostname without port. EXPECT_EQ("CONNECT example.com HTTP/1.1\r\nHost: example.com\r\n\r\n", - UpstreamHttp11ConnectSocket::formatConnectRequest("example.com")); + UpstreamHttp11ConnectSocket::formatConnectRequest("example.com", + true /* include_host_header */)); // Test with hostname with port. EXPECT_EQ("CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n", - UpstreamHttp11ConnectSocket::formatConnectRequest("example.com:443")); + UpstreamHttp11ConnectSocket::formatConnectRequest("example.com:443", + true /* include_host_header */)); // Test with IPv4 address without port. EXPECT_EQ("CONNECT 192.168.1.1 HTTP/1.1\r\nHost: 192.168.1.1\r\n\r\n", - UpstreamHttp11ConnectSocket::formatConnectRequest("192.168.1.1")); + UpstreamHttp11ConnectSocket::formatConnectRequest("192.168.1.1", + true /* include_host_header */)); // Test with IPv4 address with port. EXPECT_EQ("CONNECT 192.168.1.1:8080 HTTP/1.1\r\nHost: 192.168.1.1:8080\r\n\r\n", - UpstreamHttp11ConnectSocket::formatConnectRequest("192.168.1.1:8080")); + UpstreamHttp11ConnectSocket::formatConnectRequest("192.168.1.1:8080", + true /* include_host_header */)); // Test with IPv6 address without port. EXPECT_EQ("CONNECT [2001:db8::1] HTTP/1.1\r\nHost: [2001:db8::1]\r\n\r\n", - UpstreamHttp11ConnectSocket::formatConnectRequest("[2001:db8::1]")); + UpstreamHttp11ConnectSocket::formatConnectRequest("[2001:db8::1]", + true /* include_host_header */)); // Test with IPv6 address with port. EXPECT_EQ("CONNECT [2001:db8::1]:443 HTTP/1.1\r\nHost: [2001:db8::1]:443\r\n\r\n", - UpstreamHttp11ConnectSocket::formatConnectRequest("[2001:db8::1]:443")); + UpstreamHttp11ConnectSocket::formatConnectRequest("[2001:db8::1]:443", + true /* include_host_header */)); +} + +// Test that the formatConnectRequest() utility method omits the Host header when +// include_host_header is false. +TEST(FormatConnectRequestTest, FormatConnectRequestWithoutHostHeader) { + // Test with hostname without port. + EXPECT_EQ("CONNECT example.com HTTP/1.1\r\n\r\n", + UpstreamHttp11ConnectSocket::formatConnectRequest("example.com", + false /* include_host_header */)); + // Test with hostname with port. + EXPECT_EQ("CONNECT example.com:443 HTTP/1.1\r\n\r\n", + UpstreamHttp11ConnectSocket::formatConnectRequest("example.com:443", + false /* include_host_header */)); + + // Test with IPv4 address without port. + EXPECT_EQ("CONNECT 192.168.1.1 HTTP/1.1\r\n\r\n", + UpstreamHttp11ConnectSocket::formatConnectRequest("192.168.1.1", + false /* include_host_header */)); + + // Test with IPv4 address with port. + EXPECT_EQ("CONNECT 192.168.1.1:8080 HTTP/1.1\r\n\r\n", + UpstreamHttp11ConnectSocket::formatConnectRequest("192.168.1.1:8080", + false /* include_host_header */)); + + // Test with IPv6 address without port. + EXPECT_EQ("CONNECT [2001:db8::1] HTTP/1.1\r\n\r\n", + UpstreamHttp11ConnectSocket::formatConnectRequest("[2001:db8::1]", + false /* include_host_header */)); + + // Test with IPv6 address with port. + EXPECT_EQ("CONNECT [2001:db8::1]:443 HTTP/1.1\r\n\r\n", + UpstreamHttp11ConnectSocket::formatConnectRequest("[2001:db8::1]:443", + false /* include_host_header */)); +} + +// Test that the formatConnectRequest() utility method with an empty authorization value +// (the default) does not add a Proxy-Authorization header. +TEST(FormatConnectRequestTest, FormatConnectRequestWithEmptyAuthorization) { + // Test with the default parameter value. + EXPECT_EQ("CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n", + UpstreamHttp11ConnectSocket::formatConnectRequest("example.com:443", + true /* include_host_header */)); + + // Explicitly pass an empty authorization value. + EXPECT_EQ("CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n", + UpstreamHttp11ConnectSocket::formatConnectRequest("example.com:443", + true /* include_host_header */, "")); +} + +// Test that the formatConnectRequest() utility method with a non-empty authorization value +// adds a Proxy-Authorization header. +TEST(FormatConnectRequestTest, FormatConnectRequestWithAuthorization) { + // Test with Host header. + EXPECT_EQ("CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n" + "Proxy-Authorization: Basic dXNlcjpwYXNz\r\n\r\n", + UpstreamHttp11ConnectSocket::formatConnectRequest( + "example.com:443", true /* include_host_header */, "Basic dXNlcjpwYXNz")); + + // Test with no Host header. + EXPECT_EQ("CONNECT example.com:443 HTTP/1.1\r\nProxy-Authorization: Basic dXNlcjpwYXNz\r\n\r\n", + UpstreamHttp11ConnectSocket::formatConnectRequest( + "example.com:443", false /* include_host_header */, "Basic dXNlcjpwYXNz")); } // Test runtime guard for legacy behavior with transport socket options. @@ -745,6 +826,106 @@ TEST_P(Http11ConnectTest, RuntimeGuardLegacyBehaviorEndpointMetadata) { EXPECT_EQ(msg.length(), rc2.bytes_processed_); } +// Test that the Proxy-Authorization header is not added when +// ENVOY_HTTP11_PROXY_TRANSPORT_SOCKET_AUTH is absent from typed_filter_metadata +// (legacy format mode disabled). +TEST_P(Http11ConnectTest, NoProxyAuthHeaderWhenAuthMetadataAbsent) { + initializeWithMetadataProxyAddr(false, ""); + + const std::string address_str = + absl::StrCat(Network::Test::getLoopbackAddressUrlString(GetParam()), ":1234"); + const std::string expected_connect_string = + absl::StrCat("CONNECT ", address_str, " HTTP/1.1\r\nHost: ", address_str, "\r\n\r\n"); + Buffer::OwnedImpl expected_legacy_data{expected_connect_string}; + + EXPECT_CALL(io_handle_, write(BufferString(expected_legacy_data.toString()))) + .WillOnce(Invoke([&](Buffer::Instance& buffer) { + auto length = buffer.length(); + buffer.drain(length); + return Api::IoCallUint64Result(length, Api::IoError::none()); + })); + + Buffer::OwnedImpl msg("data"); + connect_socket_->doWrite(msg, false); +} + +// Test that the Proxy-Authorization header is added when ENVOY_HTTP11_PROXY_TRANSPORT_SOCKET_AUTH +// is present in typed_filter_metadata (legacy format mode disabled). +TEST_P(Http11ConnectTest, ProxyAuthHeaderAddedWhenAuthMetadataPresent) { + initializeWithMetadataProxyAddr(false, ProxyAuthorizationValue); + + const std::string address_str = + absl::StrCat(Network::Test::getLoopbackAddressUrlString(GetParam()), ":1234"); + const std::string expected_connect_string = + absl::StrCat("CONNECT ", address_str, " HTTP/1.1\r\nHost: ", address_str, + "\r\nProxy-Authorization: ", ProxyAuthorizationValue, "\r\n\r\n"); + Buffer::OwnedImpl expected_legacy_data{expected_connect_string}; + + EXPECT_CALL(io_handle_, write(BufferString(expected_legacy_data.toString()))) + .WillOnce(Invoke([&](Buffer::Instance& buffer) { + auto length = buffer.length(); + buffer.drain(length); + return Api::IoCallUint64Result(length, Api::IoError::none()); + })); + + Buffer::OwnedImpl msg("data"); + connect_socket_->doWrite(msg, false); +} + +// Test that the Proxy-Authorization header is not added when +// ENVOY_HTTP11_PROXY_TRANSPORT_SOCKET_AUTH is absent from typed_filter_metadata +// (legacy format mode enabled). +TEST_P(Http11ConnectTest, NoProxyAuthHeaderWhenAuthMetadataAbsentLegacyMode) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.http_11_proxy_connect_legacy_format", "true"}}); + + initializeWithMetadataProxyAddr(false, ""); + + const std::string address_str = + absl::StrCat(Network::Test::getLoopbackAddressUrlString(GetParam()), ":1234"); + const std::string expected_connect_string = + absl::StrCat("CONNECT ", address_str, " HTTP/1.1\r\n\r\n"); + Buffer::OwnedImpl expected_legacy_data{expected_connect_string}; + + EXPECT_CALL(io_handle_, write(BufferString(expected_legacy_data.toString()))) + .WillOnce(Invoke([&](Buffer::Instance& buffer) { + auto length = buffer.length(); + buffer.drain(length); + return Api::IoCallUint64Result(length, Api::IoError::none()); + })); + + Buffer::OwnedImpl msg("data"); + connect_socket_->doWrite(msg, false); +} + +// Test that the Proxy-Authorization header is added when ENVOY_HTTP11_PROXY_TRANSPORT_SOCKET_AUTH +// is present in typed_filter_metadata (legacy format mode enabled). +TEST_P(Http11ConnectTest, ProxyAuthHeaderAddedWhenAuthMetadataPresentLegacyMode) { + TestScopedRuntime scoped_runtime; + scoped_runtime.mergeValues( + {{"envoy.reloadable_features.http_11_proxy_connect_legacy_format", "true"}}); + + initializeWithMetadataProxyAddr(false, ProxyAuthorizationValue); + + const std::string address_str = + absl::StrCat(Network::Test::getLoopbackAddressUrlString(GetParam()), ":1234"); + const std::string expected_connect_string = + absl::StrCat("CONNECT ", address_str, + " HTTP/1.1\r\nProxy-Authorization: ", ProxyAuthorizationValue, "\r\n\r\n"); + Buffer::OwnedImpl expected_legacy_data{expected_connect_string}; + + EXPECT_CALL(io_handle_, write(BufferString(expected_legacy_data.toString()))) + .WillOnce(Invoke([&](Buffer::Instance& buffer) { + auto length = buffer.length(); + buffer.drain(length); + return Api::IoCallUint64Result(length, Api::IoError::none()); + })); + + Buffer::OwnedImpl msg("data"); + connect_socket_->doWrite(msg, false); +} + // Test that writes are buffered until CONNECT response is received, and then flushed by // flushWriteBuffer(). TEST_P(Http11ConnectTest, WriteFlushedAfterConnectRead) {