xref: /aosp_15_r20/external/grpc-grpc-java/xds/src/main/java/io/grpc/xds/RoutingUtils.java (revision e07d83d3ffcef9ecfc9f7f475418ec639ff0e5fe)
1 /*
2  * Copyright 2021 The gRPC Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package io.grpc.xds;
18 
19 import static com.google.common.base.Preconditions.checkArgument;
20 
21 import com.google.common.base.Joiner;
22 import io.grpc.Metadata;
23 import io.grpc.xds.VirtualHost.Route.RouteMatch;
24 import io.grpc.xds.VirtualHost.Route.RouteMatch.PathMatcher;
25 import io.grpc.xds.internal.Matchers.FractionMatcher;
26 import io.grpc.xds.internal.Matchers.HeaderMatcher;
27 import java.util.List;
28 import java.util.Locale;
29 import javax.annotation.Nullable;
30 
31 /**
32  * Utilities for performing virtual host domain name matching and route matching.
33  */
34 // TODO(chengyuanzhang): clean up implementations in XdsNameResolver.
35 final class RoutingUtils {
36   // Prevent instantiation.
RoutingUtils()37   private RoutingUtils() {
38   }
39 
40   /**
41    * Returns the {@link VirtualHost} with the best match domain for the given hostname.
42    */
43   @Nullable
findVirtualHostForHostName(List<VirtualHost> virtualHosts, String hostName)44   static VirtualHost findVirtualHostForHostName(List<VirtualHost> virtualHosts, String hostName) {
45     // Domain search order:
46     //  1. Exact domain names: ``www.foo.com``.
47     //  2. Suffix domain wildcards: ``*.foo.com`` or ``*-bar.foo.com``.
48     //  3. Prefix domain wildcards: ``foo.*`` or ``foo-*``.
49     //  4. Special wildcard ``*`` matching any domain.
50     //
51     //  The longest wildcards match first.
52     //  Assuming only a single virtual host in the entire route configuration can match
53     //  on ``*`` and a domain must be unique across all virtual hosts.
54     int matchingLen = -1; // longest length of wildcard pattern that matches host name
55     boolean exactMatchFound = false;  // true if a virtual host with exactly matched domain found
56     VirtualHost targetVirtualHost = null;  // target VirtualHost with longest matched domain
57     for (VirtualHost vHost : virtualHosts) {
58       for (String domain : vHost.domains()) {
59         boolean selected = false;
60         if (matchHostName(hostName, domain)) { // matching
61           if (!domain.contains("*")) { // exact matching
62             exactMatchFound = true;
63             targetVirtualHost = vHost;
64             break;
65           } else if (domain.length() > matchingLen) { // longer matching pattern
66             selected = true;
67           } else if (domain.length() == matchingLen && domain.startsWith("*")) { // suffix matching
68             selected = true;
69           }
70         }
71         if (selected) {
72           matchingLen = domain.length();
73           targetVirtualHost = vHost;
74         }
75       }
76       if (exactMatchFound) {
77         break;
78       }
79     }
80     return targetVirtualHost;
81   }
82 
83   /**
84    * Returns {@code true} iff {@code hostName} matches the domain name {@code pattern} with
85    * case-insensitive.
86    *
87    * <p>Wildcard pattern rules:
88    * <ol>
89    * <li>A single asterisk (*) matches any domain.</li>
90    * <li>Asterisk (*) is only permitted in the left-most or the right-most part of the pattern,
91    *     but not both.</li>
92    * </ol>
93    */
matchHostName(String hostName, String pattern)94   private static boolean matchHostName(String hostName, String pattern) {
95     checkArgument(hostName.length() != 0 && !hostName.startsWith(".") && !hostName.endsWith("."),
96         "Invalid host name");
97     checkArgument(pattern.length() != 0 && !pattern.startsWith(".") && !pattern.endsWith("."),
98         "Invalid pattern/domain name");
99 
100     hostName = hostName.toLowerCase(Locale.US);
101     pattern = pattern.toLowerCase(Locale.US);
102     // hostName and pattern are now in lower case -- domain names are case-insensitive.
103 
104     if (!pattern.contains("*")) {
105       // Not a wildcard pattern -- hostName and pattern must match exactly.
106       return hostName.equals(pattern);
107     }
108     // Wildcard pattern
109 
110     if (pattern.length() == 1) {
111       return true;
112     }
113 
114     int index = pattern.indexOf('*');
115 
116     // At most one asterisk (*) is allowed.
117     if (pattern.indexOf('*', index + 1) != -1) {
118       return false;
119     }
120 
121     // Asterisk can only match prefix or suffix.
122     if (index != 0 && index != pattern.length() - 1) {
123       return false;
124     }
125 
126     // HostName must be at least as long as the pattern because asterisk has to
127     // match one or more characters.
128     if (hostName.length() < pattern.length()) {
129       return false;
130     }
131 
132     if (index == 0 && hostName.endsWith(pattern.substring(1))) {
133       // Prefix matching fails.
134       return true;
135     }
136 
137     // Pattern matches hostname if suffix matching succeeds.
138     return index == pattern.length() - 1
139         && hostName.startsWith(pattern.substring(0, pattern.length() - 1));
140   }
141 
142   /**
143    * Returns {@code true} iff the given {@link RouteMatch} matches the RPC's full method name and
144    * headers.
145    */
matchRoute(RouteMatch routeMatch, String fullMethodName, Metadata headers, ThreadSafeRandom random)146   static boolean matchRoute(RouteMatch routeMatch, String fullMethodName,
147       Metadata headers, ThreadSafeRandom random) {
148     if (!matchPath(routeMatch.pathMatcher(), fullMethodName)) {
149       return false;
150     }
151     for (HeaderMatcher headerMatcher : routeMatch.headerMatchers()) {
152       if (!headerMatcher.matches(getHeaderValue(headers, headerMatcher.name()))) {
153         return false;
154       }
155     }
156     FractionMatcher fraction = routeMatch.fractionMatcher();
157     return fraction == null || random.nextInt(fraction.denominator()) < fraction.numerator();
158   }
159 
matchPath(PathMatcher pathMatcher, String fullMethodName)160   private static boolean matchPath(PathMatcher pathMatcher, String fullMethodName) {
161     if (pathMatcher.path() != null) {
162       return pathMatcher.caseSensitive()
163           ? pathMatcher.path().equals(fullMethodName)
164           : pathMatcher.path().equalsIgnoreCase(fullMethodName);
165     } else if (pathMatcher.prefix() != null) {
166       return pathMatcher.caseSensitive()
167           ? fullMethodName.startsWith(pathMatcher.prefix())
168           : fullMethodName.toLowerCase(Locale.US).startsWith(
169               pathMatcher.prefix().toLowerCase(Locale.US));
170     }
171     return pathMatcher.regEx().matches(fullMethodName);
172   }
173 
174   @Nullable
getHeaderValue(Metadata headers, String headerName)175   private static String getHeaderValue(Metadata headers, String headerName) {
176     if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
177       return null;
178     }
179     if (headerName.equals("content-type")) {
180       return "application/grpc";
181     }
182     Metadata.Key<String> key;
183     try {
184       key = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER);
185     } catch (IllegalArgumentException e) {
186       return null;
187     }
188     Iterable<String> values = headers.getAll(key);
189     return values == null ? null : Joiner.on(",").join(values);
190   }
191 }
192