diff --git a/xds/src/main/java/io/grpc/xds/RoutingUtils.java b/xds/src/main/java/io/grpc/xds/RoutingUtils.java index bff6756a9a4..2b60e90deda 100644 --- a/xds/src/main/java/io/grpc/xds/RoutingUtils.java +++ b/xds/src/main/java/io/grpc/xds/RoutingUtils.java @@ -92,24 +92,15 @@ static VirtualHost findVirtualHostForHostName(List virtualHosts, St * */ private static boolean matchHostName(String hostName, String pattern) { - checkArgument(hostName.length() != 0 && !hostName.startsWith("."), + checkArgument(hostName.length() != 0 && !hostName.startsWith(".") && !hostName.endsWith("."), "Invalid host name"); - checkArgument(pattern.length() != 0 && !pattern.startsWith("."), + checkArgument(pattern.length() != 0 && !pattern.startsWith(".") && !pattern.endsWith("."), "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 ec3e417e53a..196d51fb5a6 100644 --- a/xds/src/main/java/io/grpc/xds/XdsNameResolver.java +++ b/xds/src/main/java/io/grpc/xds/XdsNameResolver.java @@ -73,6 +73,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -340,6 +341,66 @@ private void updateResolutionResult(XdsConfig xdsConfig) { } } + /** + * Returns {@code true} iff {@code hostName} matches the domain name {@code pattern} with + * case-insensitive. + * + *

Wildcard pattern rules: + *

    + *
  1. A single asterisk (*) matches any domain.
  2. + *
  3. Asterisk (*) is only permitted in the left-most or the right-most part of the pattern, + * but not both.
  4. + *
+ */ + @VisibleForTesting + static boolean matchHostName(String hostName, String pattern) { + checkArgument(hostName.length() != 0 && !hostName.startsWith(".") && !hostName.endsWith("."), + "Invalid host name"); + checkArgument(pattern.length() != 0 && !pattern.startsWith(".") && !pattern.endsWith("."), + "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. + + if (!pattern.contains("*")) { + // Not a wildcard pattern -- hostName and pattern must match exactly. + return hostName.equals(pattern); + } + // Wildcard pattern + + if (pattern.length() == 1) { + return true; + } + + int index = pattern.indexOf('*'); + + // At most one asterisk (*) is allowed. + if (pattern.indexOf('*', index + 1) != -1) { + return false; + } + + // Asterisk can only match prefix or suffix. + if (index != 0 && index != pattern.length() - 1) { + return false; + } + + // HostName must be at least as long as the pattern because asterisk has to + // match one or more characters. + if (hostName.length() < pattern.length()) { + return false; + } + + if (index == 0 && hostName.endsWith(pattern.substring(1))) { + // Prefix matching fails. + return true; + } + + // Pattern matches hostname if suffix matching succeeds. + return index == pattern.length() - 1 + && hostName.startsWith(pattern.substring(0, pattern.length() - 1)); + } + private final class ConfigSelector extends InternalConfigSelector { @Override public Result selectConfig(PickSubchannelArgs args) { diff --git a/xds/src/test/java/io/grpc/xds/RoutingUtilsTest.java b/xds/src/test/java/io/grpc/xds/RoutingUtilsTest.java index e9fde9f4c4a..a460501e85b 100644 --- a/xds/src/test/java/io/grpc/xds/RoutingUtilsTest.java +++ b/xds/src/test/java/io/grpc/xds/RoutingUtilsTest.java @@ -17,7 +17,6 @@ package io.grpc.xds; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import com.google.common.collect.ImmutableMap; @@ -89,170 +88,6 @@ 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 findVirtualHostForHostName_exactMatch() { - List routes = Collections.emptyList(); - VirtualHost vHostFoo = VirtualHost.create("vhost-foo", - Collections.singletonList("foo.googleapis.com"), routes, - ImmutableMap.of()); - VirtualHost vHostBar = VirtualHost.create("vhost-bar", - Collections.singletonList("bar.googleapis.com"), routes, - ImmutableMap.of()); - List virtualHosts = - Arrays.asList(vHostFoo, vHostBar); - - assertThat(RoutingUtils.findVirtualHostForHostName( - virtualHosts, "foo.googleapis.com")).isEqualTo(vHostFoo); - assertThat(RoutingUtils.findVirtualHostForHostName( - virtualHosts, "bar.googleapis.com")).isEqualTo(vHostBar); - // No match returns null. - assertThat(RoutingUtils.findVirtualHostForHostName( - virtualHosts, "baz.googleapis.com")).isNull(); - assertThat(RoutingUtils.findVirtualHostForHostName( - virtualHosts, "foo.googleapis")).isNull(); - } - - @Test - public void findVirtualHostForHostName_invalidHostName() { - List routes = Collections.emptyList(); - VirtualHost vHost = VirtualHost.create("vhost", - Collections.singletonList("a.googleapis.com"), routes, - ImmutableMap.of()); - List virtualHosts = Collections.singletonList(vHost); - - // Empty hostName. - assertThrows(IllegalArgumentException.class, - () -> RoutingUtils.findVirtualHostForHostName( - virtualHosts, "")); - // HostName starting with dot. - assertThrows(IllegalArgumentException.class, - () -> RoutingUtils.findVirtualHostForHostName( - virtualHosts, ".a.googleapis.com")); - } - - @Test - public void findVirtualHostForHostName_invalidPattern() { - List routes = Collections.emptyList(); - // Empty domain pattern. - VirtualHost vHostEmpty = VirtualHost.create("vhost-empty", - Collections.singletonList(""), routes, - ImmutableMap.of()); - assertThrows(IllegalArgumentException.class, - () -> RoutingUtils.findVirtualHostForHostName( - Collections.singletonList(vHostEmpty), - "a.googleapis.com")); - // Domain pattern starting with dot. - VirtualHost vHostDot = VirtualHost.create("vhost-dot", - Collections.singletonList(".a.googleapis.com"), routes, - ImmutableMap.of()); - assertThrows(IllegalArgumentException.class, - () -> RoutingUtils.findVirtualHostForHostName( - Collections.singletonList(vHostDot), - "a.googleapis.com")); - } - - @Test - public void findVirtualHostForHostName_prefixWildcard() { - List routes = Collections.emptyList(); - VirtualHost vHostWild = VirtualHost.create("vhost-wild", - Collections.singletonList("*.foo.googleapis.com"), - routes, ImmutableMap.of()); - VirtualHost vHostOther = VirtualHost.create("vhost-other", - Collections.singletonList("other.googleapis.com"), - routes, ImmutableMap.of()); - List virtualHosts = - Arrays.asList(vHostWild, vHostOther); - - // Prefix wildcard matches. - assertThat(RoutingUtils.findVirtualHostForHostName( - virtualHosts, "bar.foo.googleapis.com")) - .isEqualTo(vHostWild); - // Base domain without subdomain does not match *.foo.googleapis.com. - assertThat(RoutingUtils.findVirtualHostForHostName( - virtualHosts, "foo.googleapis.com")).isNull(); - - // Longer prefix wildcard is preferred over shorter one. - VirtualHost vHostLong = VirtualHost.create("vhost-long", - Collections.singletonList("*.bar.foo.googleapis.com"), - routes, ImmutableMap.of()); - List virtualHosts2 = - Arrays.asList(vHostLong, vHostWild); - assertThat(RoutingUtils.findVirtualHostForHostName( - virtualHosts2, "baz.bar.foo.googleapis.com")) - .isEqualTo(vHostLong); - } - - @Test - public void findVirtualHostForHostName_postfixWildcard() { - List routes = Collections.emptyList(); - VirtualHost vHostWild = VirtualHost.create("vhost-wild", - Collections.singletonList("foo.*"), routes, - ImmutableMap.of()); - VirtualHost vHostOther = VirtualHost.create("vhost-other", - Collections.singletonList("bar.googleapis.com"), - routes, ImmutableMap.of()); - List virtualHosts = - Arrays.asList(vHostWild, vHostOther); - - // Postfix wildcard matches. - assertThat(RoutingUtils.findVirtualHostForHostName( - virtualHosts, "foo.googleapis.com")) - .isEqualTo(vHostWild); - assertThat(RoutingUtils.findVirtualHostForHostName( - virtualHosts, "foo.com")).isEqualTo(vHostWild); - // Different prefix does not match foo.*. - assertThat(RoutingUtils.findVirtualHostForHostName( - virtualHosts, "bar.foo.googleapis.com")).isNull(); - } - @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 3f50d92c2b5..45a96ee172f 100644 --- a/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java +++ b/xds/src/test/java/io/grpc/xds/XdsNameResolverTest.java @@ -2020,6 +2020,48 @@ public void generateServiceConfig_forPerMethodConfig() throws IOException { .isEqualTo(expectedServiceConfig); } + @Test + public void matchHostName_exactlyMatch() { + String pattern = "foo.googleapis.com"; + assertThat(XdsNameResolver.matchHostName("bar.googleapis.com", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("fo.googleapis.com", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("oo.googleapis.com", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("googleapis.com", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("foo.googleapis", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("foo.googleapis.com", pattern)).isTrue(); + } + + @Test + public void matchHostName_prefixWildcard() { + String pattern = "*.foo.googleapis.com"; + assertThat(XdsNameResolver.matchHostName("foo.googleapis.com", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("bar-baz.foo.googleapis", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("bar.foo.googleapis.com", pattern)).isTrue(); + pattern = "*-bar.foo.googleapis.com"; + assertThat(XdsNameResolver.matchHostName("bar.foo.googleapis.com", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("baz-bar.foo.googleapis", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("-bar.foo.googleapis.com", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("baz-bar.foo.googleapis.com", pattern)) + .isTrue(); + } + + @Test + public void matchHostName_postfixWildCard() { + String pattern = "foo.*"; + assertThat(XdsNameResolver.matchHostName("bar.googleapis.com", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("bar.foo.googleapis.com", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("foo.googleapis.com", pattern)).isTrue(); + assertThat(XdsNameResolver.matchHostName("foo.com", pattern)).isTrue(); + pattern = "foo-*"; + assertThat(XdsNameResolver.matchHostName("bar-.googleapis.com", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("foo.googleapis.com", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("foo.googleapis.com", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("foo-", pattern)).isFalse(); + assertThat(XdsNameResolver.matchHostName("foo-bar.com", pattern)).isTrue(); + assertThat(XdsNameResolver.matchHostName("foo-.com", pattern)).isTrue(); + assertThat(XdsNameResolver.matchHostName("foo-bar", pattern)).isTrue(); + } + @Test public void resolved_faultAbortInLdsUpdate() { resolver.start(mockListener);