diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 3c868585fc29..6d635a4da12c 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -109,6 +109,7 @@ New Features * access log: support command operator: %FILTER_CHAIN_NAME% for the downstream tcp and http request. * access log: support command operator: %REQUEST_HEADERS_BYTES%, %RESPONSE_HEADERS_BYTES%, and %RESPONSE_TRAILERS_BYTES%. * compression: add brotli :ref:`compressor ` and :ref:`decompressor `. +* compression: extended the compression allow compressing when the content length header is not present. This behavior may be temporarily reverted by setting `envoy.reloadable_features.enable_compression_without_content_length_header` to false. * config: add `envoy.features.fail_on_any_deprecated_feature` runtime key, which matches the behaviour of compile-time flag `ENVOY_DISABLE_DEPRECATED_FEATURES`, i.e. use of deprecated fields will cause a crash. * config: the ``Node`` :ref:`dynamic context parameters ` are populated in discovery requests when set on the server instance. * dispatcher: supports a stack of `Envoy::ScopeTrackedObject` instead of a single tracked object. This will allow Envoy to dump more debug information on crash. diff --git a/source/common/runtime/runtime_features.cc b/source/common/runtime/runtime_features.cc index be68b6260617..530b308b4f94 100644 --- a/source/common/runtime/runtime_features.cc +++ b/source/common/runtime/runtime_features.cc @@ -66,6 +66,7 @@ constexpr const char* runtime_features[] = { "envoy.reloadable_features.check_ocsp_policy", "envoy.reloadable_features.disable_tls_inspector_injection", "envoy.reloadable_features.dont_add_content_length_for_bodiless_requests", + "envoy.reloadable_features.enable_compression_without_content_length_header", "envoy.reloadable_features.grpc_web_fix_non_proto_encoded_response_handling", "envoy.reloadable_features.hcm_stream_error_on_invalid_message", "envoy.reloadable_features.health_check.graceful_goaway_handling", diff --git a/source/extensions/filters/http/common/compressor/BUILD b/source/extensions/filters/http/common/compressor/BUILD index a1c67b984a5e..ec0648006b5c 100644 --- a/source/extensions/filters/http/common/compressor/BUILD +++ b/source/extensions/filters/http/common/compressor/BUILD @@ -19,6 +19,7 @@ envoy_cc_library( "//include/envoy/stream_info:filter_state_interface", "//source/common/buffer:buffer_lib", "//source/common/http:header_map_lib", + "//source/common/http:utility_lib", "//source/common/protobuf", "//source/common/runtime:runtime_lib", "//source/extensions/filters/http/common:pass_through_filter_lib", diff --git a/source/extensions/filters/http/common/compressor/compressor.cc b/source/extensions/filters/http/common/compressor/compressor.cc index c14c42ed6eca..7f9f5fe8e0a0 100644 --- a/source/extensions/filters/http/common/compressor/compressor.cc +++ b/source/extensions/filters/http/common/compressor/compressor.cc @@ -2,6 +2,7 @@ #include "common/buffer/buffer_impl.h" #include "common/http/header_map_impl.h" +#include "common/http/utility.h" namespace Envoy { namespace Extensions { @@ -156,7 +157,12 @@ Http::FilterHeadersStatus CompressorFilter::decodeHeaders(Http::RequestHeaderMap } const auto& request_config = config_->requestDirectionConfig(); - if (!end_stream && request_config.compressionEnabled() && + const bool is_not_upgrade = + !Http::Utility::isUpgrade(headers) || + !Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.enable_compression_without_content_length_header"); + + if (!end_stream && request_config.compressionEnabled() && is_not_upgrade && request_config.isMinimumContentLength(headers) && request_config.isContentTypeAllowed(headers) && !headers.getInline(request_content_encoding_handle.handle()) && @@ -221,7 +227,12 @@ Http::FilterHeadersStatus CompressorFilter::encodeHeaders(Http::ResponseHeaderMa const auto& config = config_->responseDirectionConfig(); const bool isEnabledAndContentLengthBigEnough = config.compressionEnabled() && config.isMinimumContentLength(headers); - const bool isCompressible = isEnabledAndContentLengthBigEnough && + const bool is_not_upgrade = + !Http::Utility::isUpgrade(headers) || + !Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.enable_compression_without_content_length_header"); + + const bool isCompressible = isEnabledAndContentLengthBigEnough && is_not_upgrade && config.isContentTypeAllowed(headers) && !hasCacheControlNoTransform(headers) && isEtagAllowed(headers) && !headers.getInline(response_content_encoding_handle.handle()); @@ -506,7 +517,11 @@ bool CompressorFilterConfig::DirectionConfig::isMinimumContentLength( } return is_minimum_content_length; } - + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.enable_compression_without_content_length_header")) { + // return true to ignore the minimum length configuration if no content-length header is present + return true; + } return StringUtil::caseFindToken(headers.getTransferEncodingValue(), ",", Http::Headers::get().TransferEncodingValues.Chunked); } diff --git a/test/extensions/filters/http/common/compressor/BUILD b/test/extensions/filters/http/common/compressor/BUILD index a6b214dd6b50..6d984e2245ed 100644 --- a/test/extensions/filters/http/common/compressor/BUILD +++ b/test/extensions/filters/http/common/compressor/BUILD @@ -21,6 +21,7 @@ envoy_cc_test( "//test/mocks/http:http_mocks", "//test/mocks/protobuf:protobuf_mocks", "//test/mocks/runtime:runtime_mocks", + "//test/test_common:test_runtime_lib", "//test/test_common:utility_lib", "@envoy_api//envoy/extensions/filters/http/compressor/v3:pkg_cc_proto", ], @@ -46,6 +47,25 @@ envoy_cc_benchmark_binary( ], ) +envoy_cc_test( + name = "compressor_integration_tests", + srcs = [ + "compressor_integration_tests.cc", + "compressor_integration_tests.h", + ], + deps = [ + "//source/common/http:header_map_lib", + "//source/extensions/access_loggers/file:config", + "//source/extensions/compression/gzip/compressor:config", + "//source/extensions/filters/http/buffer:config", + "//source/extensions/filters/http/compressor:config", + "//test/integration:http_protocol_integration_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", + ], +) + envoy_benchmark_test( name = "compressor_filter_speed_test_benchmark_test", benchmark_binary = "compressor_filter_speed_test", diff --git a/test/extensions/filters/http/common/compressor/compressor_filter_test.cc b/test/extensions/filters/http/common/compressor/compressor_filter_test.cc index c8b4b79093e7..37513d45a968 100644 --- a/test/extensions/filters/http/common/compressor/compressor_filter_test.cc +++ b/test/extensions/filters/http/common/compressor/compressor_filter_test.cc @@ -11,6 +11,7 @@ #include "test/mocks/protobuf/mocks.h" #include "test/mocks/runtime/mocks.h" #include "test/mocks/stats/mocks.h" +#include "test/test_common/test_runtime.h" #include "test/test_common/utility.h" #include "gtest/gtest.h" @@ -77,8 +78,14 @@ class CompressorFilterTest : public testing::Test { } void verifyCompressedData() { - EXPECT_EQ(expected_str_.length(), stats_.counter("test.test.total_uncompressed_bytes").value()); - EXPECT_EQ(data_.length(), stats_.counter("test.test.total_compressed_bytes").value()); + EXPECT_EQ( + expected_str_.length(), + stats_.counter(fmt::format("test.test.{}total_uncompressed_bytes", response_stats_prefix_)) + .value()); + EXPECT_EQ( + data_.length(), + stats_.counter(fmt::format("test.test.{}total_compressed_bytes", response_stats_prefix_)) + .value()); } void populateBuffer(uint64_t size) { @@ -132,9 +139,6 @@ class CompressorFilterTest : public testing::Test { bool with_trailers) { uint64_t buffer_content_size; if (!absl::SimpleAtoi(headers.get_("content-length"), &buffer_content_size)) { - ASSERT_TRUE( - StringUtil::CaseInsensitiveCompare()(headers.get_("transfer-encoding"), "chunked")); - // In case of chunked stream just feed the buffer with 1000 bytes. buffer_content_size = 1000; } populateBuffer(buffer_content_size); @@ -156,7 +160,8 @@ class CompressorFilterTest : public testing::Test { EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->encodeTrailers(trailers)); } verifyCompressedData(); - EXPECT_EQ(1, stats_.counter("test.test.compressed").value()); + EXPECT_EQ( + 1, stats_.counter(fmt::format("test.test.{}compressed", response_stats_prefix_)).value()); } else { EXPECT_EQ("", headers.get_("content-encoding")); EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(data_, false)); @@ -233,6 +238,47 @@ TEST_F(CompressorFilterTest, CompressRequest) { doResponseNoCompression(headers); } +TEST_F(CompressorFilterTest, CompressRequestAndResponseNoContentLength) { + setUpFilter(R"EOF( +{ + "request_direction_config": {}, + "response_direction_config": {}, + "compressor_library": { + "name": "test", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip" + } + } +} +)EOF"); + response_stats_prefix_ = "response."; + doRequestCompression({{":method", "post"}, {"accept-encoding", "deflate, test"}}, false); + Http::TestResponseHeaderMapImpl headers{{":status", "200"}}; + doResponseCompression(headers, false); +} + +TEST_F(CompressorFilterTest, CompressRequestAndResponseNoContentLengthRuntimeDisabled) { + setUpFilter(R"EOF( +{ + "request_direction_config": {}, + "response_direction_config": {}, + "compressor_library": { + "name": "test", + "typed_config": { + "@type": "type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip" + } + } +} +)EOF"); + TestScopedRuntime scoped_runtime; + Runtime::LoaderSingleton::getExisting()->mergeValues( + {{"envoy.reloadable_features.enable_compression_without_content_length_header", "false"}}); + response_stats_prefix_ = "response."; + doRequestNoCompression({{":method", "get"}, {"accept-encoding", "deflate, test"}}); + Http::TestResponseHeaderMapImpl headers{{":status", "200"}}; + doResponseNoCompression(headers); +} + TEST_F(CompressorFilterTest, CompressRequestWithTrailers) { setUpFilter(R"EOF( { @@ -552,10 +598,7 @@ INSTANTIATE_TEST_SUITE_P( IsMinimumContentLengthTestSuite, IsMinimumContentLengthTest, testing::Values(std::make_tuple("content-length", "31", "", true), std::make_tuple("content-length", "29", "", false), - std::make_tuple("transfer-encoding", "chunked", "", true), - std::make_tuple("transfer-encoding", "Chunked", "", true), - std::make_tuple("transfer-encoding", "chunked", "\"content_length\": 500,", - true), + std::make_tuple("", "", "\"content_length\": 500,", true), std::make_tuple("content-length", "501", "\"content_length\": 500,", true), std::make_tuple("content-length", "499", "\"content_length\": 500,", false))); @@ -590,9 +633,7 @@ class IsTransferEncodingAllowedTest INSTANTIATE_TEST_SUITE_P( IsTransferEncodingAllowedSuite, IsTransferEncodingAllowedTest, - testing::Values(std::make_tuple("transfer-encoding", "chunked", true), - std::make_tuple("transfer-encoding", "Chunked", true), - std::make_tuple("transfer-encoding", "deflate", false), + testing::Values(std::make_tuple("transfer-encoding", "deflate", false), std::make_tuple("transfer-encoding", "Deflate", false), std::make_tuple("transfer-encoding", "test", false), std::make_tuple("transfer-encoding", "chunked, test", false), diff --git a/test/extensions/filters/http/common/compressor/compressor_integration_tests.cc b/test/extensions/filters/http/common/compressor/compressor_integration_tests.cc new file mode 100644 index 000000000000..1ee14acf3cd4 --- /dev/null +++ b/test/extensions/filters/http/common/compressor/compressor_integration_tests.cc @@ -0,0 +1,297 @@ +#include "test/extensions/filters/http/common/compressor/compressor_integration_tests.h" + +#include + +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" + +#include "common/http/header_map_impl.h" +#include "common/protobuf/utility.h" + +#include "test/integration/utility.h" +#include "test/test_common/network_utility.h" +#include "test/test_common/printers.h" +#include "test/test_common/utility.h" + +#include "absl/strings/str_cat.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace { + +Http::TestRequestHeaderMapImpl upgradeRequestHeaders(const char* upgrade_type = "websocket", + uint32_t content_length = 0) { + return Http::TestRequestHeaderMapImpl{{":authority", "host"}, + {"content-length", fmt::format("{}", content_length)}, + {":path", "/websocket/test"}, + {":method", "GET"}, + {":scheme", "http"}, + {"upgrade", upgrade_type}, + {"connection", "keep-alive, upgrade"}}; +} + +Http::TestResponseHeaderMapImpl upgradeResponseHeaders(const char* upgrade_type = "websocket") { + return Http::TestResponseHeaderMapImpl{ + {":status", "101"}, {"connection", "upgrade"}, {"upgrade", upgrade_type}}; +} + +const std::string compressorFilterConfig = R"EOF( +name: envoy.filters.http.compressor +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor + request_direction_config: + response_direction_config: + compressor_library: + name: test + typed_config: + "@type": type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip +)EOF"; + +template +void commonValidate(ProxiedHeaders& proxied_headers, const OriginalHeaders& original_headers) { + // If no content length is specified, the HTTP1 codec will add a chunked encoding header. + if (original_headers.ContentLength() == nullptr && + proxied_headers.TransferEncoding() != nullptr) { + ASSERT_EQ(proxied_headers.getTransferEncodingValue(), "chunked"); + proxied_headers.removeTransferEncoding(); + } + if (proxied_headers.Connection() != nullptr && + proxied_headers.Connection()->value() == "upgrade" && + original_headers.Connection() != nullptr && + original_headers.Connection()->value() == "keep-alive, upgrade") { + // The keep-alive is implicit for HTTP/1.1, so Envoy only sets the upgrade + // header when converting from HTTP/1.1 to H2 + proxied_headers.setConnection("keep-alive, upgrade"); + } +} + +} // namespace + +void WebsocketWithCompressorIntegrationTest::validateUpgradeRequestHeaders( + const Http::RequestHeaderMap& original_proxied_request_headers, + const Http::RequestHeaderMap& original_request_headers) { + Http::TestRequestHeaderMapImpl proxied_request_headers(original_proxied_request_headers); + if (proxied_request_headers.ForwardedProto()) { + ASSERT_EQ(proxied_request_headers.getForwardedProtoValue(), "http"); + proxied_request_headers.removeForwardedProto(); + } + + // Check for and remove headers added by default for HTTP requests. + ASSERT_TRUE(proxied_request_headers.RequestId() != nullptr); + ASSERT_TRUE(proxied_request_headers.EnvoyExpectedRequestTimeoutMs() != nullptr); + proxied_request_headers.removeEnvoyExpectedRequestTimeoutMs(); + + if (proxied_request_headers.Scheme()) { + ASSERT_EQ(proxied_request_headers.getSchemeValue(), "http"); + } else { + proxied_request_headers.setScheme("http"); + } + + // 0 byte content lengths may be stripped on the H2 path - ignore that as a difference by adding + // it back to the proxied headers. + if (original_request_headers.ContentLength() && + proxied_request_headers.ContentLength() == nullptr) { + proxied_request_headers.setContentLength(size_t(0)); + } + + commonValidate(proxied_request_headers, original_request_headers); + proxied_request_headers.removeRequestId(); + + EXPECT_THAT(&proxied_request_headers, HeaderMapEqualIgnoreOrder(&original_request_headers)); +} + +void WebsocketWithCompressorIntegrationTest::validateUpgradeResponseHeaders( + const Http::ResponseHeaderMap& original_proxied_response_headers, + const Http::ResponseHeaderMap& original_response_headers) { + Http::TestResponseHeaderMapImpl proxied_response_headers(original_proxied_response_headers); + + // Check for and remove headers added by default for HTTP responses. + ASSERT_TRUE(proxied_response_headers.Date() != nullptr); + ASSERT_TRUE(proxied_response_headers.Server() != nullptr); + ASSERT_EQ(proxied_response_headers.getServerValue(), "envoy"); + proxied_response_headers.removeDate(); + proxied_response_headers.removeServer(); + + ASSERT_TRUE(proxied_response_headers.TransferEncoding() == nullptr); + + commonValidate(proxied_response_headers, original_response_headers); + + EXPECT_THAT(&proxied_response_headers, HeaderMapEqualIgnoreOrder(&original_response_headers)); +} + +INSTANTIATE_TEST_SUITE_P(Protocols, WebsocketWithCompressorIntegrationTest, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams()), + HttpProtocolIntegrationTest::protocolTestParamsToString); + +ConfigHelper::HttpModifierFunction setRouteUsingWebsocket() { + return [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { hcm.add_upgrade_configs()->set_upgrade_type("websocket"); }; +} + +void WebsocketWithCompressorIntegrationTest::initialize() { + if (upstreamProtocol() != FakeHttpConnection::Type::HTTP1) { + config_helper_.addConfigModifier( + [&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + ConfigHelper::HttpProtocolOptions protocol_options; + protocol_options.mutable_explicit_http_config() + ->mutable_http2_protocol_options() + ->set_allow_connect(true); + ConfigHelper::setProtocolOptions( + *bootstrap.mutable_static_resources()->mutable_clusters(0), protocol_options); + }); + } + if (downstreamProtocol() != Http::CodecClient::Type::HTTP1) { + config_helper_.addConfigModifier( + [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) -> void { hcm.mutable_http2_protocol_options()->set_allow_connect(true); }); + } + config_helper_.addFilter(compressorFilterConfig); + HttpProtocolIntegrationTest::initialize(); +} + +void WebsocketWithCompressorIntegrationTest::performUpgrade( + const Http::TestRequestHeaderMapImpl& upgrade_request_headers, + const Http::TestResponseHeaderMapImpl& upgrade_response_headers) { + // Establish the initial connection. + codec_client_ = makeHttpConnection(lookupPort("http")); + + // Send websocket upgrade request + auto encoder_decoder = codec_client_->startRequest(upgrade_request_headers); + request_encoder_ = &encoder_decoder.first; + response_ = std::move(encoder_decoder.second); + test_server_->waitForCounterGe("http.config_test.downstream_cx_upgrades_total", 1); + test_server_->waitForGaugeGe("http.config_test.downstream_cx_upgrades_active", 1); + + // Verify the upgrade was received upstream. + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); + validateUpgradeRequestHeaders(upstream_request_->headers(), upgrade_request_headers); + + // Send the upgrade response + upstream_request_->encodeHeaders(upgrade_response_headers, false); + + // Verify the upgrade response was received downstream. + response_->waitForHeaders(); + validateUpgradeResponseHeaders(response_->headers(), upgrade_response_headers); +} + +void WebsocketWithCompressorIntegrationTest::sendBidirectionalData() { + // Verify that the client can still send data upstream, and that upstream + // receives it. + codec_client_->sendData(*request_encoder_, "hello", false); + ASSERT_TRUE(upstream_request_->waitForData(*dispatcher_, "hello")); + + // Verify the upstream can send data to the client and that the client + // receives it. + upstream_request_->encodeData("world", false); + response_->waitForBodyData(5); + EXPECT_EQ("world", response_->body()); +} + +// Technically not a websocket tests, but verifies normal upgrades have parity +// with websocket upgrades +TEST_P(WebsocketWithCompressorIntegrationTest, NonWebsocketUpgrade) { + config_helper_.addConfigModifier( + [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) -> void { + auto* foo_upgrade = hcm.add_upgrade_configs(); + foo_upgrade->set_upgrade_type("foo"); + }); + + config_helper_.addConfigModifier(setRouteUsingWebsocket()); + initialize(); + + performUpgrade(upgradeRequestHeaders("foo", 0), upgradeResponseHeaders("foo")); + sendBidirectionalData(); + codec_client_->sendData(*request_encoder_, "bye!", false); + if (downstreamProtocol() == Http::CodecClient::Type::HTTP1) { + codec_client_->close(); + } else { + codec_client_->sendReset(*request_encoder_); + } + + ASSERT_TRUE(upstream_request_->waitForData(*dispatcher_, "hellobye!")); + ASSERT_TRUE(waitForUpstreamDisconnectOrReset()); + + auto upgrade_response_headers(upgradeResponseHeaders("foo")); + validateUpgradeResponseHeaders(response_->headers(), upgrade_response_headers); + codec_client_->close(); + + auto request_compressed_counter = + test_server_->counter("http.config_test.compressor.test.gzip.request.compressed"); + ASSERT_NE(request_compressed_counter, nullptr); + ASSERT_EQ(0, request_compressed_counter->value()); + + auto request_uncompressed_counter = + test_server_->counter("http.config_test.compressor.test.gzip.request.not_compressed"); + ASSERT_NE(request_uncompressed_counter, nullptr); + ASSERT_EQ(1, request_uncompressed_counter->value()); + + auto response_compressed_counter = + test_server_->counter("http.config_test.compressor.test.gzip.compressed"); + ASSERT_NE(response_compressed_counter, nullptr); + ASSERT_EQ(0, response_compressed_counter->value()); + + auto response_uncompressed_counter = + test_server_->counter("http.config_test.compressor.test.gzip.not_compressed"); + ASSERT_NE(response_uncompressed_counter, nullptr); + ASSERT_EQ(1, response_uncompressed_counter->value()); +} + +INSTANTIATE_TEST_SUITE_P(Protocols, CompressorProxyingConnectIntegrationTest, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams()), + HttpProtocolIntegrationTest::protocolTestParamsToString); + +void CompressorProxyingConnectIntegrationTest::initialize() { + config_helper_.addConfigModifier( + [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) -> void { ConfigHelper::setConnectConfig(hcm, false, false); }); + config_helper_.addFilter(compressorFilterConfig); + HttpProtocolIntegrationTest::initialize(); +} + +TEST_P(CompressorProxyingConnectIntegrationTest, ProxyConnect) { + initialize(); + + // Send request headers. + codec_client_ = makeHttpConnection(lookupPort("http")); + auto encoder_decoder = codec_client_->startRequest(connect_headers_); + request_encoder_ = &encoder_decoder.first; + response_ = std::move(encoder_decoder.second); + + // Wait for them to arrive upstream. + AssertionResult result = + fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_); + RELEASE_ASSERT(result, result.message()); + result = fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_); + RELEASE_ASSERT(result, result.message()); + ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); + EXPECT_EQ(upstream_request_->headers().get(Http::Headers::get().Method)[0]->value(), "CONNECT"); + if (upstreamProtocol() == FakeHttpConnection::Type::HTTP1) { + EXPECT_TRUE(upstream_request_->headers().get(Http::Headers::get().Protocol).empty()); + } else { + EXPECT_EQ(upstream_request_->headers().get(Http::Headers::get().Protocol)[0]->value(), + "bytestream"); + } + + // Send response headers + upstream_request_->encodeHeaders(default_response_headers_, false); + + // Wait for them to arrive downstream. + response_->waitForHeaders(); + EXPECT_EQ("200", response_->headers().getStatusValue()); + + // Make sure that even once the response has started, that data can continue to go upstream. + codec_client_->sendData(*request_encoder_, "hello", false); + ASSERT_TRUE(upstream_request_->waitForData(*dispatcher_, "hello")); + + // Also test upstream to downstream data. + upstream_request_->encodeData("world", false); + response_->waitForBodyData(5); + EXPECT_EQ("world", response_->body()); + + cleanupUpstreamAndDownstream(); +} + +} // namespace Envoy diff --git a/test/extensions/filters/http/common/compressor/compressor_integration_tests.h b/test/extensions/filters/http/common/compressor/compressor_integration_tests.h new file mode 100644 index 000000000000..43bd52e50d74 --- /dev/null +++ b/test/extensions/filters/http/common/compressor/compressor_integration_tests.h @@ -0,0 +1,55 @@ +#pragma once + +#include "envoy/http/codec.h" + +#include "test/integration/http_protocol_integration.h" + +#include "gtest/gtest.h" + +namespace Envoy { + +/* +Test verifying that we don't add any unexpecting headers when we are trying to upgrade connection +to a websocket connection +*/ +class WebsocketWithCompressorIntegrationTest : public HttpProtocolIntegrationTest { +public: + void initialize() override; + +protected: + void performUpgrade(const Http::TestRequestHeaderMapImpl& upgrade_request_headers, + const Http::TestResponseHeaderMapImpl& upgrade_response_headers); + void sendBidirectionalData(); + + void validateUpgradeRequestHeaders(const Http::RequestHeaderMap& proxied_request_headers, + const Http::RequestHeaderMap& original_request_headers); + void validateUpgradeResponseHeaders(const Http::ResponseHeaderMap& proxied_response_headers, + const Http::ResponseHeaderMap& original_response_headers); + + ABSL_MUST_USE_RESULT + testing::AssertionResult waitForUpstreamDisconnectOrReset() { + if (upstreamProtocol() != FakeHttpConnection::Type::HTTP1) { + return upstream_request_->waitForReset(); + } else { + return fake_upstream_connection_->waitForDisconnect(); + } + } + + IntegrationStreamDecoderPtr response_; +}; + +/* +Test verifying that we don't break proxying of CONNECT method when compressor filter is enabled +*/ +class CompressorProxyingConnectIntegrationTest : public HttpProtocolIntegrationTest { +public: + void initialize() override; + Http::TestRequestHeaderMapImpl connect_headers_{{":method", "CONNECT"}, + {":path", "/"}, + {":protocol", "bytestream"}, + {":scheme", "https"}, + {":authority", "host:80"}}; + IntegrationStreamDecoderPtr response_; +}; + +} // namespace Envoy