diff --git a/xds/src/main/java/io/grpc/xds/RoutingUtils.java b/xds/src/main/java/io/grpc/xds/RoutingUtils.java index 2b60e90deda..bff6756a9a4 100644 --- a/xds/src/main/java/io/grpc/xds/RoutingUtils.java +++ b/xds/src/main/java/io/grpc/xds/RoutingUtils.java @@ -92,15 +92,24 @@ static VirtualHost findVirtualHostForHostName(List virtualHosts, St * */ private static boolean matchHostName(String hostName, String pattern) { - checkArgument(hostName.length() != 0 && !hostName.startsWith(".") && !hostName.endsWith("."), + checkArgument(hostName.length() != 0 && !hostName.startsWith("."), "Invalid host name"); - checkArgument(pattern.length() != 0 && !pattern.startsWith(".") && !pattern.endsWith("."), + checkArgument(pattern.length() != 0 && !pattern.startsWith("."), "Invalid pattern/domain name"); hostName = hostName.toLowerCase(Locale.US); pattern = pattern.toLowerCase(Locale.US); // hostName and pattern are now in lower case -- domain names are case-insensitive. + // Strip trailing dot to normalize FQDN (e.g. "example.com.") to a relative form, + // as per RFC 1034 Section 3.1 the two are semantically equivalent. + if (hostName.endsWith(".")) { + hostName = hostName.substring(0, hostName.length() - 1); + } + if (pattern.endsWith(".")) { + pattern = pattern.substring(0, pattern.length() - 1); + } + if (!pattern.contains("*")) { // Not a wildcard pattern -- hostName and pattern must match exactly. return hostName.equals(pattern); diff --git a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java index 196d51fb5a6..3021c035d04 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -354,15 +354,24 @@ private void updateResolutionResult(XdsConfig xdsConfig) { */ @VisibleForTesting static boolean matchHostName(String hostName, String pattern) { - checkArgument(hostName.length() != 0 && !hostName.startsWith(".") && !hostName.endsWith("."), + checkArgument(hostName.length() != 0 && !hostName.startsWith("."), "Invalid host name"); - checkArgument(pattern.length() != 0 && !pattern.startsWith(".") && !pattern.endsWith("."), + checkArgument(pattern.length() != 0 && !pattern.startsWith("."), "Invalid pattern/domain name"); hostName = hostName.toLowerCase(Locale.US); pattern = pattern.toLowerCase(Locale.US); // hostName and pattern are now in lower case -- domain names are case-insensitive. + // Strip trailing dot to normalize FQDN (e.g. "example.com.") to a relative form, + // as per RFC 1034 Section 3.1 the two are semantically equivalent. + if (hostName.endsWith(".")) { + hostName = hostName.substring(0, hostName.length() - 1); + } + if (pattern.endsWith(".")) { + pattern = pattern.substring(0, pattern.length() - 1); + } + if (!pattern.contains("*")) { // Not a wildcard pattern -- hostName and pattern must match exactly. return hostName.equals(pattern); diff --git a/xds/src/test/java/io/grpc/xds/RoutingUtilsTest.java b/xds/src/test/java/io/grpc/xds/RoutingUtilsTest.java index a460501e85b..cd1afccbb9d 100644 --- a/xds/src/test/java/io/grpc/xds/RoutingUtilsTest.java +++ b/xds/src/test/java/io/grpc/xds/RoutingUtilsTest.java @@ -88,6 +88,54 @@ public void findVirtualHostForHostName_asteriskMatchAnyDomain() { .isEqualTo(vHost1); } + @Test + public void findVirtualHostForHostName_trailingDot() { + // FQDN (trailing dot) is semantically equivalent to the relative form + // per RFC 1034 Section 3.1. + List routes = Collections.emptyList(); + VirtualHost vHost1 = VirtualHost.create("virtualhost01.googleapis.com", + Collections.singletonList("a.googleapis.com"), routes, + ImmutableMap.of()); + VirtualHost vHost2 = VirtualHost.create("virtualhost02.googleapis.com", + Collections.singletonList("*.googleapis.com"), routes, + ImmutableMap.of()); + VirtualHost vHost3 = VirtualHost.create("virtualhost03.googleapis.com", + Collections.singletonList("*"), routes, + ImmutableMap.of()); + List virtualHosts = Arrays.asList(vHost1, vHost2, vHost3); + + // Trailing dot in hostName, exact match. + assertThat(RoutingUtils.findVirtualHostForHostName( + virtualHosts, "a.googleapis.com.")).isEqualTo(vHost1); + // Trailing dot in hostName, wildcard match. + assertThat(RoutingUtils.findVirtualHostForHostName( + virtualHosts, "b.googleapis.com.")).isEqualTo(vHost2); + + // Trailing dot in domain pattern, exact match. + VirtualHost vHost4 = VirtualHost.create("virtualhost04.googleapis.com", + Collections.singletonList("a.googleapis.com."), routes, + ImmutableMap.of()); + List virtualHosts2 = + Arrays.asList(vHost4, vHost2, vHost3); + assertThat(RoutingUtils.findVirtualHostForHostName( + virtualHosts2, "a.googleapis.com")).isEqualTo(vHost4); + + // Trailing dot in both hostName and domain pattern. + assertThat(RoutingUtils.findVirtualHostForHostName( + virtualHosts2, "a.googleapis.com.")).isEqualTo(vHost4); + + // Trailing dot in domain pattern, wildcard match. + VirtualHost vHost5 = VirtualHost.create("virtualhost05.googleapis.com", + Collections.singletonList("*.googleapis.com."), routes, + ImmutableMap.of()); + List virtualHosts3 = + Arrays.asList(vHost5, vHost3); + assertThat(RoutingUtils.findVirtualHostForHostName( + virtualHosts3, "b.googleapis.com")).isEqualTo(vHost5); + assertThat(RoutingUtils.findVirtualHostForHostName( + virtualHosts3, "b.googleapis.com.")).isEqualTo(vHost5); + } + @Test public void routeMatching_pathOnly() { Metadata headers = new Metadata(); diff --git a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java index 45a96ee172f..86fcddfc62d 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -2062,6 +2062,29 @@ public void matchHostName_postfixWildCard() { assertThat(XdsNameResolver.matchHostName("foo-bar", pattern)).isTrue(); } + @Test + public void matchHostName_trailingDot() { + // FQDN (trailing dot) is semantically equivalent to the relative form per RFC 1034 Section 3.1. + assertThat(XdsNameResolver.matchHostName("foo.googleapis.com.", "foo.googleapis.com")).isTrue(); + assertThat(XdsNameResolver.matchHostName("foo.googleapis.com", "foo.googleapis.com.")).isTrue(); + assertThat(XdsNameResolver.matchHostName("foo.googleapis.com.", "foo.googleapis.com.")) + .isTrue(); + assertThat(XdsNameResolver.matchHostName("bar.googleapis.com.", "foo.googleapis.com")) + .isFalse(); + + // Wildcard + trailing dot combinations. + String pattern = "*.foo.googleapis.com"; + assertThat(XdsNameResolver.matchHostName("bar.foo.googleapis.com.", pattern)).isTrue(); + assertThat(XdsNameResolver.matchHostName("bar.foo.googleapis.com", pattern + ".")).isTrue(); + assertThat(XdsNameResolver.matchHostName("bar.foo.googleapis.com.", pattern + ".")).isTrue(); + assertThat(XdsNameResolver.matchHostName("foo.googleapis.com.", pattern)).isFalse(); + + pattern = "foo.*"; + assertThat(XdsNameResolver.matchHostName("foo.googleapis.com.", pattern)).isTrue(); + assertThat(XdsNameResolver.matchHostName("foo.com.", pattern)).isTrue(); + assertThat(XdsNameResolver.matchHostName("bar.googleapis.com.", pattern)).isFalse(); + } + @Test public void resolved_faultAbortInLdsUpdate() { resolver.start(mockListener);