diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index df141cb13087b..856249c2a25ac 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -35,7 +35,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 47] +// [#next-free-field: 48] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"; @@ -702,6 +702,16 @@ message HttpConnectionManager { // ` // for details. PathNormalizationOptions path_normalization_options = 43; + + // Determines if trailing dot of the host should be removed from host/authority header before any + // processing of request by HTTP filters or routing. + // This affects the upstream host header. + // Without setting this option, incoming requests with host `example.com.` will not match against + // route with :ref:`domains` match set to `example.com`. Defaults to `false`. + // When the incoming request contains a host/authority header that includes a port number, + // setting this option will strip a trailing dot, if present, from the host section, + // leaving the port as is (e.g. host value `example.com.:443` will be updated to `example.com:443`). + bool strip_trailing_host_dot = 47; } // The configuration to customize local reply returned by Envoy. diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto b/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto index 37f15c7d49633..c9f4333f3c7cb 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto +++ b/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto @@ -33,7 +33,7 @@ option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSIO // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 47] +// [#next-free-field: 48] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"; @@ -680,6 +680,16 @@ message HttpConnectionManager { // ` // for details. PathNormalizationOptions path_normalization_options = 43; + + // Determines if trailing dot of the host should be removed from host/authority header before any + // processing of request by HTTP filters or routing. + // This affects the upstream host header. + // Without setting this option, incoming requests with host `example.com.` will not match against + // route with :ref:`domains` match set to `example.com`. Defaults to `false`. + // When the incoming request contains a host/authority header that includes a port number, + // setting this option will strip a trailing dot, if present, from the host section, + // leaving the port as is (e.g. host value `example.com.:443` will be updated to `example.com:443`). + bool strip_trailing_host_dot = 47; } // The configuration to customize local reply returned by Envoy. diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 449d76c1bd2c0..b163f1edc1656 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -67,6 +67,7 @@ New Features value is `true`, the unsupported http filter will be ignored by envoy. This is also same with unsupported http filter in the typed per filter config. For more information, please reference :ref:`HttpFilter `. +* http: added :ref:`stripping trailing host dot from host header` support. * http: added support for :ref:`original IP detection extensions`. Two initial extensions were added, the :ref:`custom header ` extension and the :ref:`xff ` extension. diff --git a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index 76c8488c32be6..095f3a28d7124 100644 --- a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -36,7 +36,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 47] +// [#next-free-field: 48] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"; @@ -708,6 +708,16 @@ message HttpConnectionManager { // for details. PathNormalizationOptions path_normalization_options = 43; + // Determines if trailing dot of the host should be removed from host/authority header before any + // processing of request by HTTP filters or routing. + // This affects the upstream host header. + // Without setting this option, incoming requests with host `example.com.` will not match against + // route with :ref:`domains` match set to `example.com`. Defaults to `false`. + // When the incoming request contains a host/authority header that includes a port number, + // setting this option will strip a trailing dot, if present, from the host section, + // leaving the port as is (e.g. host value `example.com.:443` will be updated to `example.com:443`). + bool strip_trailing_host_dot = 47; + google.protobuf.Duration hidden_envoy_deprecated_idle_timeout = 11 [ deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0", diff --git a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto index e2f85d4b3e15d..db628cc585ec1 100644 --- a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto +++ b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto @@ -34,7 +34,7 @@ option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSIO // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 47] +// [#next-free-field: 48] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"; @@ -704,6 +704,16 @@ message HttpConnectionManager { // ` // for details. PathNormalizationOptions path_normalization_options = 43; + + // Determines if trailing dot of the host should be removed from host/authority header before any + // processing of request by HTTP filters or routing. + // This affects the upstream host header. + // Without setting this option, incoming requests with host `example.com.` will not match against + // route with :ref:`domains` match set to `example.com`. Defaults to `false`. + // When the incoming request contains a host/authority header that includes a port number, + // setting this option will strip a trailing dot, if present, from the host section, + // leaving the port as is (e.g. host value `example.com.:443` will be updated to `example.com:443`). + bool strip_trailing_host_dot = 47; } // The configuration to customize local reply returned by Envoy. diff --git a/source/common/http/conn_manager_config.h b/source/common/http/conn_manager_config.h index 8b05b4bbff49b..0569921eb9287 100644 --- a/source/common/http/conn_manager_config.h +++ b/source/common/http/conn_manager_config.h @@ -484,6 +484,12 @@ class ConnectionManagerConfig { */ virtual const std::vector& originalIpDetectionExtensions() const PURE; + + /** + * @return if the HttpConnectionManager should remove trailing host dot from host/authority + * header. + */ + virtual bool shouldStripTrailingHostDot() const PURE; }; } // namespace Http } // namespace Envoy diff --git a/source/common/http/conn_manager_utility.cc b/source/common/http/conn_manager_utility.cc index 1131f40113bb6..887fe09bcd095 100644 --- a/source/common/http/conn_manager_utility.cc +++ b/source/common/http/conn_manager_utility.cc @@ -499,6 +499,9 @@ ConnectionManagerUtility::maybeNormalizePath(RequestHeaderMap& request_headers, absl::optional ConnectionManagerUtility::maybeNormalizeHost(RequestHeaderMap& request_headers, const ConnectionManagerConfig& config, uint32_t port) { + if (config.shouldStripTrailingHostDot()) { + HeaderUtility::stripTrailingHostDot(request_headers); + } if (config.stripPortType() == Http::StripPortType::Any) { return HeaderUtility::stripPortFromHost(request_headers, absl::nullopt); } else if (config.stripPortType() == Http::StripPortType::MatchingHost) { diff --git a/source/common/http/header_utility.cc b/source/common/http/header_utility.cc index e504a21ad8a23..122d9cb254808 100644 --- a/source/common/http/header_utility.cc +++ b/source/common/http/header_utility.cc @@ -216,6 +216,25 @@ bool HeaderUtility::isEnvoyInternalRequest(const RequestHeaderMap& headers) { internal_request_header->value() == Headers::get().EnvoyInternalRequestValues.True; } +void HeaderUtility::stripTrailingHostDot(RequestHeaderMap& headers) { + auto host = headers.getHostValue(); + // If the host ends in a period, remove it. + auto dot_index = host.rfind('.'); + if (dot_index == std::string::npos) { + return; + } else if (dot_index == (host.size() - 1)) { + host.remove_suffix(1); + headers.setHost(host); + return; + } + // If the dot is just before a colon, it must be preceding the port number. + // IPv6 addresses may contain colons or dots, but the dot will never directly + // precede the colon, so this check should be sufficient to detect a trailing port number. + if (host[dot_index + 1] == ':') { + headers.setHost(absl::StrCat(host.substr(0, dot_index), host.substr(dot_index + 1))); + } +} + absl::optional HeaderUtility::stripPortFromHost(RequestHeaderMap& headers, absl::optional listener_port) { if (headers.getMethodValue() == Http::Headers::get().MethodValues.Connect && diff --git a/source/common/http/header_utility.h b/source/common/http/header_utility.h index 20719003733c2..e928c827c72be 100644 --- a/source/common/http/header_utility.h +++ b/source/common/http/header_utility.h @@ -176,6 +176,11 @@ class HeaderUtility { static bool shouldCloseConnection(Http::Protocol protocol, const RequestOrResponseHeaderMap& headers); + /** + * @brief Remove the trailing host dot from host/authority header. + */ + static void stripTrailingHostDot(RequestHeaderMap& headers); + /** * @brief Remove the port part from host/authority header if it is equal to provided port. * @return absl::optional containing the port, if removed, else absl::nullopt. diff --git a/source/extensions/filters/network/http_connection_manager/config.cc b/source/extensions/filters/network/http_connection_manager/config.cc index 67003b945d6c2..9995ac7e7fea2 100644 --- a/source/extensions/filters/network/http_connection_manager/config.cc +++ b/source/extensions/filters/network/http_connection_manager/config.cc @@ -307,7 +307,8 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( headers_with_underscores_action_( config.common_http_protocol_options().headers_with_underscores_action()), local_reply_(LocalReply::Factory::create(config.local_reply_config(), context)), - path_with_escaped_slashes_action_(getPathWithEscapedSlashesAction(config, context)) { + path_with_escaped_slashes_action_(getPathWithEscapedSlashesAction(config, context)), + strip_trailing_host_dot_(config.strip_trailing_host_dot()) { // If idle_timeout_ was not configured in common_http_protocol_options, use value in deprecated // idle_timeout field. // TODO(asraa): Remove when idle_timeout is removed. diff --git a/source/extensions/filters/network/http_connection_manager/config.h b/source/extensions/filters/network/http_connection_manager/config.h index b98629ee8ecc2..1e4e93445f61d 100644 --- a/source/extensions/filters/network/http_connection_manager/config.h +++ b/source/extensions/filters/network/http_connection_manager/config.h @@ -175,6 +175,7 @@ class HttpConnectionManagerConfig : Logger::Loggable, const Http::Http1Settings& http1Settings() const override { return http1_settings_; } bool shouldNormalizePath() const override { return normalize_path_; } bool shouldMergeSlashes() const override { return merge_slashes_; } + bool shouldStripTrailingHostDot() const override { return strip_trailing_host_dot_; } Http::StripPortType stripPortType() const override { return strip_port_type_; } envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headersWithUnderscoresAction() const override { @@ -283,6 +284,7 @@ class HttpConnectionManagerConfig : Logger::Loggable, static const uint64_t RequestHeaderTimeoutMs = 0; const envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager:: PathWithEscapedSlashesAction path_with_escaped_slashes_action_; + const bool strip_trailing_host_dot_; }; /** diff --git a/source/server/admin/admin.h b/source/server/admin/admin.h index 84d5236e212bc..5faa06b141438 100644 --- a/source/server/admin/admin.h +++ b/source/server/admin/admin.h @@ -177,6 +177,7 @@ class AdminImpl : public Admin, const Http::Http1Settings& http1Settings() const override { return http1_settings_; } bool shouldNormalizePath() const override { return true; } bool shouldMergeSlashes() const override { return true; } + bool shouldStripTrailingHostDot() const override { return false; } Http::StripPortType stripPortType() const override { return Http::StripPortType::None; } envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headersWithUnderscoresAction() const override { diff --git a/test/common/http/conn_manager_impl_fuzz_test.cc b/test/common/http/conn_manager_impl_fuzz_test.cc index a62a208315ae4..dd9c7e1525984 100644 --- a/test/common/http/conn_manager_impl_fuzz_test.cc +++ b/test/common/http/conn_manager_impl_fuzz_test.cc @@ -198,6 +198,7 @@ class FuzzConfig : public ConnectionManagerConfig { const Http::Http1Settings& http1Settings() const override { return http1_settings_; } bool shouldNormalizePath() const override { return false; } bool shouldMergeSlashes() const override { return false; } + bool shouldStripTrailingHostDot() const override { return false; } Http::StripPortType stripPortType() const override { return Http::StripPortType::None; } envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headersWithUnderscoresAction() const override { diff --git a/test/common/http/conn_manager_impl_test.cc b/test/common/http/conn_manager_impl_test.cc index 7f34287cf2ec0..177bbb8f1b345 100644 --- a/test/common/http/conn_manager_impl_test.cc +++ b/test/common/http/conn_manager_impl_test.cc @@ -1194,6 +1194,47 @@ TEST_F(HttpConnectionManagerImplTest, RouteShouldUseNormalizedHost) { filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::RemoteClose); } +// Observe that we strip the trailing dot. +TEST_F(HttpConnectionManagerImplTest, StripTrailingHostDot) { + setup(false, ""); + // Enable removal of host's trailing dot. + strip_trailing_host_dot_ = true; + const std::string original_host = "host."; + const std::string updated_host = "host"; + // Set up the codec. + Buffer::OwnedImpl fake_input("1234"); + conn_manager_->createCodec(fake_input); + // Create a new stream. + decoder_ = &conn_manager_->newStream(response_encoder_); + RequestHeaderMapPtr headers{new TestRequestHeaderMapImpl{ + {":authority", original_host}, {":path", "/"}, {":method", "GET"}}}; + RequestHeaderMap* updated_headers = headers.get(); + decoder_->decodeHeaders(std::move(headers), true); + EXPECT_EQ(updated_host, updated_headers->getHostValue()); + // Clean up. + filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::RemoteClose); +} + +TEST_F(HttpConnectionManagerImplTest, HostWithoutTrailingDot) { + setup(false, ""); + // Enable removal of host's trailing dot. + strip_trailing_host_dot_ = true; + const std::string original_host = "host"; + const std::string updated_host = "host"; + // Set up the codec. + Buffer::OwnedImpl fake_input("1234"); + conn_manager_->createCodec(fake_input); + // Create a new stream. + decoder_ = &conn_manager_->newStream(response_encoder_); + RequestHeaderMapPtr headers{new TestRequestHeaderMapImpl{ + {":authority", original_host}, {":path", "/"}, {":method", "GET"}}}; + RequestHeaderMap* updated_headers = headers.get(); + decoder_->decodeHeaders(std::move(headers), true); + EXPECT_EQ(updated_host, updated_headers->getHostValue()); + // Clean up. + filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::RemoteClose); +} + TEST_F(HttpConnectionManagerImplTest, DateHeaderNotPresent) { setup(false, ""); setUpEncoderAndDecoder(false, false); diff --git a/test/common/http/conn_manager_impl_test_base.h b/test/common/http/conn_manager_impl_test_base.h index c8e174853fb4e..46ff48516dc51 100644 --- a/test/common/http/conn_manager_impl_test_base.h +++ b/test/common/http/conn_manager_impl_test_base.h @@ -136,6 +136,7 @@ class HttpConnectionManagerImplTest : public testing::Test, public ConnectionMan const Http::Http1Settings& http1Settings() const override { return http1_settings_; } bool shouldNormalizePath() const override { return normalize_path_; } bool shouldMergeSlashes() const override { return merge_slashes_; } + bool shouldStripTrailingHostDot() const override { return strip_trailing_host_dot_; } Http::StripPortType stripPortType() const override { return strip_port_type_; } const RequestIDExtensionSharedPtr& requestIDExtension() override { return request_id_extension_; } envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction @@ -230,6 +231,7 @@ class HttpConnectionManagerImplTest : public testing::Test, public ConnectionMan PathWithEscapedSlashesAction path_with_escaped_slashes_action_{ envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager:: KEEP_UNCHANGED}; + bool strip_trailing_host_dot_ = false; }; } // namespace Http diff --git a/test/common/http/conn_manager_utility_test.cc b/test/common/http/conn_manager_utility_test.cc index 7dc2a957ebad9..e28e4ffd63084 100644 --- a/test/common/http/conn_manager_utility_test.cc +++ b/test/common/http/conn_manager_utility_test.cc @@ -1502,6 +1502,51 @@ TEST_F(ConnectionManagerUtilityTest, RemovePort) { EXPECT_EQ(header_map_none.getHostValue(), "host:9999"); } +// maybeNormalizeHost() removes trailing dot of host from host header. +TEST_F(ConnectionManagerUtilityTest, RemoveTrailingDot) { + ON_CALL(config_, shouldStripTrailingHostDot()).WillByDefault(Return(true)); + TestRequestHeaderMapImpl original_headers; + original_headers.setHost("host."); + + TestRequestHeaderMapImpl header_map(original_headers); + ConnectionManagerUtility::maybeNormalizeHost(header_map, config_, 0); + EXPECT_EQ(header_map.getHostValue(), "host"); + + ON_CALL(config_, stripPortType()).WillByDefault(Return(Http::StripPortType::None)); + ON_CALL(config_, shouldStripTrailingHostDot()).WillByDefault(Return(true)); + TestRequestHeaderMapImpl original_headers_with_port; + original_headers_with_port.setHost("host.:443"); + + TestRequestHeaderMapImpl header_map_with_port(original_headers_with_port); + ConnectionManagerUtility::maybeNormalizeHost(header_map_with_port, config_, 443); + EXPECT_EQ(header_map_with_port.getHostValue(), "host:443"); + + ON_CALL(config_, stripPortType()).WillByDefault(Return(Http::StripPortType::MatchingHost)); + ON_CALL(config_, shouldStripTrailingHostDot()).WillByDefault(Return(true)); + TestRequestHeaderMapImpl original_headers_strip_port; + original_headers_strip_port.setHost("host.:443"); + + TestRequestHeaderMapImpl header_map_strip_port(original_headers_strip_port); + ConnectionManagerUtility::maybeNormalizeHost(header_map_strip_port, config_, 443); + EXPECT_EQ(header_map_strip_port.getHostValue(), "host"); + + ON_CALL(config_, shouldStripTrailingHostDot()).WillByDefault(Return(true)); + TestRequestHeaderMapImpl original_headers_no_dot; + original_headers_no_dot.setHost("host"); + + TestRequestHeaderMapImpl header_map_no_dot(original_headers_no_dot); + ConnectionManagerUtility::maybeNormalizeHost(header_map_no_dot, config_, 0); + EXPECT_EQ(header_map_no_dot.getHostValue(), "host"); + + ON_CALL(config_, shouldStripTrailingHostDot()).WillByDefault(Return(false)); + TestRequestHeaderMapImpl original_headers_none; + original_headers_none.setHost("host."); + + TestRequestHeaderMapImpl header_map_none(original_headers_none); + ConnectionManagerUtility::maybeNormalizeHost(header_map_none, config_, 0); + EXPECT_EQ(header_map_none.getHostValue(), "host."); +} + // maybeNormalizePath() does not touch escaped slashes when configured to KEEP_UNCHANGED. TEST_F(ConnectionManagerUtilityTest, KeepEscapedSlashesWhenConfigured) { ON_CALL(config_, pathWithEscapedSlashesAction()) diff --git a/test/common/http/header_utility_test.cc b/test/common/http/header_utility_test.cc index a529f7011b010..6204009429afa 100644 --- a/test/common/http/header_utility_test.cc +++ b/test/common/http/header_utility_test.cc @@ -114,6 +114,34 @@ TEST_F(HeaderUtilityTest, RemovePortsFromHostConnectLegacy) { } } +// Host's trailing dot from host header get removed. +TEST_F(HeaderUtilityTest, RemoveTrailingDotFromHost) { + const std::vector> host_headers{ + {"localhost", "localhost"}, // w/o dot + {"localhost.", "localhost"}, // name w/ dot + {"", ""}, // empty + {"192.168.1.1", "192.168.1.1"}, // ipv4 + {"abc.com", "abc.com"}, // dns w/o dot + {"abc.com.", "abc.com"}, // dns w/ dot + {"abc.com:443", "abc.com:443"}, // dns port w/o dot + {"abc.com.:443", "abc.com:443"}, // dns port w/ dot + {"[fc00::1]", "[fc00::1]"}, // ipv6 + {":", ":"}, // malformed string #1 + {"]:", "]:"}, // malformed string #2 + {":abc", ":abc"}, // malformed string #3 + {".", ""}, // malformed string #4 + {"..", "."}, // malformed string #5 + {".123", ".123"}, // malformed string #6 + {".:.", ".:"} // malformed string #7 + }; + + for (const auto& host_pair : host_headers) { + auto& host_header = hostHeaderEntry(host_pair.first); + HeaderUtility::stripTrailingHostDot(headers_); + EXPECT_EQ(host_header.value().getStringView(), host_pair.second); + } +} + TEST(GetAllOfHeaderAsStringTest, All) { const LowerCaseString test_header("test"); { diff --git a/test/extensions/filters/network/http_connection_manager/config_test.cc b/test/extensions/filters/network/http_connection_manager/config_test.cc index 7d2a11ceb7f0e..6137c357640e7 100644 --- a/test/extensions/filters/network/http_connection_manager/config_test.cc +++ b/test/extensions/filters/network/http_connection_manager/config_test.cc @@ -1152,6 +1152,59 @@ TEST_F(HttpConnectionManagerConfigTest, RemoveAnyPortFalse) { EXPECT_EQ(Http::StripPortType::None, config.stripPortType()); } +// Validated that by default we don't remove host's trailing dot. +TEST_F(HttpConnectionManagerConfigTest, RemoveTrailingDotDefault) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + route_config: + name: local_route + http_filters: + - name: envoy.filters.http.router + )EOF"; + + HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); + EXPECT_EQ(false, config.shouldStripTrailingHostDot()); +} + +// Validated that when configured, we remove host's trailing dot. +TEST_F(HttpConnectionManagerConfigTest, RemoveTrailingDotTrue) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + route_config: + name: local_route + strip_trailing_host_dot: true + http_filters: + - name: envoy.filters.http.router + )EOF"; + + HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); + EXPECT_EQ(true, config.shouldStripTrailingHostDot()); +} + +// Validated that when explicitly set false, then we don't remove trailing host dot. +TEST_F(HttpConnectionManagerConfigTest, RemoveTrailingDotFalse) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + route_config: + name: local_route + strip_trailing_host_dot: false + http_filters: + - name: envoy.filters.http.router + )EOF"; + + HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + scoped_routes_config_provider_manager_, http_tracer_manager_, + filter_config_provider_manager_); + EXPECT_EQ(false, config.shouldStripTrailingHostDot()); +} + // Validated that by default we allow requests with header names containing underscores. TEST_F(HttpConnectionManagerConfigTest, HeadersWithUnderscoresAllowedByDefault) { const std::string yaml_string = R"EOF( diff --git a/test/integration/protocol_integration_test.cc b/test/integration/protocol_integration_test.cc index 3c799a0566a55..f2245a6c02a96 100644 --- a/test/integration/protocol_integration_test.cc +++ b/test/integration/protocol_integration_test.cc @@ -2332,4 +2332,59 @@ TEST_P(DownstreamProtocolIntegrationTest, LocalReplyWithMetadata) { ASSERT_EQ("200", response->headers().getStatusValue()); } +// Verify that host's trailing dot is removed and matches the domain for routing request. +TEST_P(ProtocolIntegrationTest, EnableStripTrailingHostDot) { + config_helper_.addConfigModifier( + [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) -> void { + hcm.set_strip_trailing_host_dot(true); + // clear existing domains and add new domain. + auto* route_config = hcm.mutable_route_config(); + auto* virtual_host = route_config->mutable_virtual_hosts(0); + virtual_host->clear_domains(); + virtual_host->add_domains("host"); + }); + + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeHeaderOnlyRequest( + Http::TestRequestHeaderMapImpl{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host."}}); + waitForNextUpstreamRequest(); + + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); +} + +// Verify that host's trailing dot is not removed and thus fails to match configured domains for +// routing request. +TEST_P(DownstreamProtocolIntegrationTest, DisableStripTrailingHostDot) { + config_helper_.addConfigModifier( + [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) -> void { + hcm.set_strip_trailing_host_dot(false); + // clear existing domains and add new domain. + auto* route_config = hcm.mutable_route_config(); + auto* virtual_host = route_config->mutable_virtual_hosts(0); + virtual_host->clear_domains(); + virtual_host->add_domains("host"); + }); + + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeHeaderOnlyRequest( + Http::TestRequestHeaderMapImpl{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host."}}); + // Expect local reply as request host fails to match configured domains. + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("404", response->headers().getStatusValue()); +} + } // namespace Envoy diff --git a/test/mocks/http/mocks.h b/test/mocks/http/mocks.h index 366170ba33b27..ec2216287b175 100644 --- a/test/mocks/http/mocks.h +++ b/test/mocks/http/mocks.h @@ -585,6 +585,7 @@ class MockConnectionManagerConfig : public ConnectionManagerConfig { MOCK_METHOD(const Http::Http1Settings&, http1Settings, (), (const)); MOCK_METHOD(bool, shouldNormalizePath, (), (const)); MOCK_METHOD(bool, shouldMergeSlashes, (), (const)); + MOCK_METHOD(bool, shouldStripTrailingHostDot, (), (const)); MOCK_METHOD(Http::StripPortType, stripPortType, (), (const)); MOCK_METHOD(envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction, headersWithUnderscoresAction, (), (const));