Skip to content

Commit ef90da0

Browse files
authored
xds: support case insensitive path matching (#7506)
1 parent 67b5460 commit ef90da0

File tree

5 files changed

+92
-140
lines changed

5 files changed

+92
-140
lines changed

xds/src/main/java/io/grpc/xds/EnvoyProtoData.java

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -919,16 +919,6 @@ RouteAction getRouteAction() {
919919
return routeAction;
920920
}
921921

922-
// TODO(chengyuanzhang): delete and do not use after routing feature is always ON.
923-
boolean isDefaultRoute() {
924-
// For backward compatibility, all the other matchers are ignored.
925-
String prefix = routeMatch.getPathMatch().getPrefix();
926-
if (prefix != null) {
927-
return prefix.isEmpty() || prefix.equals("/");
928-
}
929-
return false;
930-
}
931-
932922
@Override
933923
public boolean equals(Object o) {
934924
if (this == o) {
@@ -993,17 +983,12 @@ static StructOrError<Route> fromEnvoyProtoRoute(
993983
}
994984

995985
@VisibleForTesting
996-
@SuppressWarnings("deprecation")
997986
@Nullable
998987
static StructOrError<RouteMatch> convertEnvoyProtoRouteMatch(
999988
io.envoyproxy.envoy.config.route.v3.RouteMatch proto) {
1000989
if (proto.getQueryParametersCount() != 0) {
1001990
return null;
1002991
}
1003-
if (proto.hasCaseSensitive() && !proto.getCaseSensitive().getValue()) {
1004-
return StructOrError.fromError("Unsupported match option: case insensitive");
1005-
}
1006-
1007992
StructOrError<PathMatcher> pathMatch = convertEnvoyProtoPathMatcher(proto);
1008993
if (pathMatch.getErrorDetail() != null) {
1009994
return StructOrError.fromError(pathMatch.getErrorDetail());
@@ -1033,32 +1018,28 @@ static StructOrError<RouteMatch> convertEnvoyProtoRouteMatch(
10331018
pathMatch.getStruct(), Collections.unmodifiableList(headerMatchers), fractionMatch));
10341019
}
10351020

1036-
@SuppressWarnings("deprecation")
10371021
private static StructOrError<PathMatcher> convertEnvoyProtoPathMatcher(
10381022
io.envoyproxy.envoy.config.route.v3.RouteMatch proto) {
1039-
String path = null;
1040-
String prefix = null;
1041-
Pattern safeRegEx = null;
1023+
boolean caseSensitive = proto.getCaseSensitive().getValue();
10421024
switch (proto.getPathSpecifierCase()) {
10431025
case PREFIX:
1044-
prefix = proto.getPrefix();
1045-
break;
1026+
return StructOrError.fromStruct(
1027+
PathMatcher.fromPrefix(proto.getPrefix(), caseSensitive));
10461028
case PATH:
1047-
path = proto.getPath();
1048-
break;
1029+
return StructOrError.fromStruct(PathMatcher.fromPath(proto.getPath(), caseSensitive));
10491030
case SAFE_REGEX:
10501031
String rawPattern = proto.getSafeRegex().getRegex();
1032+
Pattern safeRegEx;
10511033
try {
10521034
safeRegEx = Pattern.compile(rawPattern);
10531035
} catch (PatternSyntaxException e) {
10541036
return StructOrError.fromError("Malformed safe regex pattern: " + e.getMessage());
10551037
}
1056-
break;
1038+
return StructOrError.fromStruct(PathMatcher.fromRegEx(safeRegEx));
10571039
case PATHSPECIFIER_NOT_SET:
10581040
default:
10591041
return StructOrError.fromError("Unknown path match type");
10601042
}
1061-
return StructOrError.fromStruct(new PathMatcher(path, prefix, safeRegEx));
10621043
}
10631044

10641045
private static StructOrError<FractionMatcher> convertEnvoyProtoFraction(

xds/src/main/java/io/grpc/xds/RouteMatch.java

Lines changed: 25 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,8 @@ final class RouteMatch {
4545
this.headerMatchers = headerMatchers;
4646
}
4747

48-
@VisibleForTesting
49-
RouteMatch(@Nullable String pathPrefixMatch, @Nullable String pathExactMatch) {
50-
this(
51-
new PathMatcher(pathExactMatch, pathPrefixMatch, null),
48+
static RouteMatch withPathExactOnly(String pathExact) {
49+
return new RouteMatch(PathMatcher.fromPath(pathExact, true),
5250
Collections.<HeaderMatcher>emptyList(), null);
5351
}
5452

@@ -82,19 +80,6 @@ boolean matches(String path, Map<String, Iterable<String>> headers) {
8280
return fractionMatch == null || fractionMatch.matches();
8381
}
8482

85-
PathMatcher getPathMatch() {
86-
return pathMatch;
87-
}
88-
89-
List<HeaderMatcher> getHeaderMatchers() {
90-
return Collections.unmodifiableList(headerMatchers);
91-
}
92-
93-
@Nullable
94-
FractionMatcher getFractionMatch() {
95-
return fractionMatch;
96-
}
97-
9883
@Override
9984
public boolean equals(Object o) {
10085
if (this == o) {
@@ -132,35 +117,37 @@ static final class PathMatcher {
132117
private final String prefix;
133118
@Nullable
134119
private final Pattern regEx;
120+
private final boolean caseSensitive;
135121

136-
PathMatcher(@Nullable String path, @Nullable String prefix, @Nullable Pattern regEx) {
122+
private PathMatcher(@Nullable String path, @Nullable String prefix, @Nullable Pattern regEx,
123+
boolean caseSensitive) {
137124
this.path = path;
138125
this.prefix = prefix;
139126
this.regEx = regEx;
127+
this.caseSensitive = caseSensitive;
140128
}
141129

142-
private boolean matches(String fullMethodName) {
143-
if (path != null) {
144-
return path.equals(fullMethodName);
145-
} else if (prefix != null) {
146-
return fullMethodName.startsWith(prefix);
147-
}
148-
return regEx.matches(fullMethodName);
130+
static PathMatcher fromPath(String path, boolean caseSensitive) {
131+
return new PathMatcher(path, null, null, caseSensitive);
149132
}
150133

151-
@Nullable
152-
String getPath() {
153-
return path;
134+
static PathMatcher fromPrefix(String prefix, boolean caseSensitive) {
135+
return new PathMatcher(null, prefix, null, caseSensitive);
154136
}
155137

156-
@Nullable
157-
String getPrefix() {
158-
return prefix;
138+
static PathMatcher fromRegEx(Pattern regEx) {
139+
return new PathMatcher(null, null, regEx, false /* doesn't matter */);
159140
}
160141

161-
@Nullable
162-
Pattern getRegEx() {
163-
return regEx;
142+
boolean matches(String fullMethodName) {
143+
if (path != null) {
144+
return caseSensitive ? path.equals(fullMethodName) : path.equalsIgnoreCase(fullMethodName);
145+
} else if (prefix != null) {
146+
return caseSensitive
147+
? fullMethodName.startsWith(prefix)
148+
: fullMethodName.toLowerCase().startsWith(prefix.toLowerCase());
149+
}
150+
return regEx.matches(fullMethodName);
164151
}
165152

166153
@Override
@@ -174,25 +161,26 @@ public boolean equals(Object o) {
174161
PathMatcher that = (PathMatcher) o;
175162
return Objects.equals(path, that.path)
176163
&& Objects.equals(prefix, that.prefix)
164+
&& Objects.equals(caseSensitive, that.caseSensitive)
177165
&& Objects.equals(
178166
regEx == null ? null : regEx.pattern(),
179167
that.regEx == null ? null : that.regEx.pattern());
180168
}
181169

182170
@Override
183171
public int hashCode() {
184-
return Objects.hash(path, prefix, regEx == null ? null : regEx.pattern());
172+
return Objects.hash(path, prefix, caseSensitive, regEx == null ? null : regEx.pattern());
185173
}
186174

187175
@Override
188176
public String toString() {
189177
ToStringHelper toStringHelper =
190178
MoreObjects.toStringHelper(this);
191179
if (path != null) {
192-
toStringHelper.add("path", path);
180+
toStringHelper.add("path", path).add("caseSensitive", caseSensitive);
193181
}
194182
if (prefix != null) {
195-
toStringHelper.add("prefix", prefix);
183+
toStringHelper.add("prefix", prefix).add("caseSensitive", caseSensitive);
196184
}
197185
if (regEx != null) {
198186
toStringHelper.add("regEx", regEx.pattern());

xds/src/test/java/io/grpc/xds/EnvoyProtoDataTest.java

Lines changed: 19 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
import java.util.Arrays;
5151
import java.util.Collections;
5252
import java.util.concurrent.TimeUnit;
53-
import javax.annotation.Nullable;
5453
import org.junit.Test;
5554
import org.junit.runner.RunWith;
5655
import org.junit.runners.JUnit4;
@@ -211,7 +210,7 @@ public void convertRoute() {
211210
assertThat(struct1.getStruct())
212211
.isEqualTo(
213212
new Route(
214-
new RouteMatch(new PathMatcher("/service/method", null, null),
213+
new RouteMatch(PathMatcher.fromPath("/service/method", false),
215214
Collections.<HeaderMatcher>emptyList(), null),
216215
new RouteAction(TimeUnit.SECONDS.toNanos(15L), "cluster-foo", null)));
217216

@@ -259,38 +258,6 @@ public void convertRoute_skipWithUnsupportedAction() {
259258
assertThat(Route.fromEnvoyProtoRoute(proto)).isNull();
260259
}
261260

262-
@Test
263-
public void isDefaultRoute() {
264-
StructOrError<Route> struct1 = Route.fromEnvoyProtoRoute(buildSimpleRouteProto("", null));
265-
StructOrError<Route> struct2 = Route.fromEnvoyProtoRoute(buildSimpleRouteProto("/", null));
266-
StructOrError<Route> struct3 =
267-
Route.fromEnvoyProtoRoute(buildSimpleRouteProto("/service/", null));
268-
StructOrError<Route> struct4 =
269-
Route.fromEnvoyProtoRoute(buildSimpleRouteProto(null, "/service/method"));
270-
271-
assertThat(struct1.getStruct().isDefaultRoute()).isTrue();
272-
assertThat(struct2.getStruct().isDefaultRoute()).isTrue();
273-
assertThat(struct3.getStruct().isDefaultRoute()).isFalse();
274-
assertThat(struct4.getStruct().isDefaultRoute()).isFalse();
275-
}
276-
277-
private static io.envoyproxy.envoy.config.route.v3.Route buildSimpleRouteProto(
278-
@Nullable String pathPrefix, @Nullable String path) {
279-
io.envoyproxy.envoy.config.route.v3.Route.Builder routeBuilder =
280-
io.envoyproxy.envoy.config.route.v3.Route.newBuilder()
281-
.setName("simple-route")
282-
.setRoute(io.envoyproxy.envoy.config.route.v3.RouteAction.newBuilder()
283-
.setCluster("simple-cluster"));
284-
if (pathPrefix != null) {
285-
routeBuilder.setMatch(io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder()
286-
.setPrefix(pathPrefix));
287-
} else if (path != null) {
288-
routeBuilder.setMatch(io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder()
289-
.setPath(path));
290-
}
291-
return routeBuilder.build();
292-
}
293-
294261
@Test
295262
public void convertRouteMatch_pathMatching() {
296263
// path_specifier = prefix
@@ -300,7 +267,13 @@ public void convertRouteMatch_pathMatching() {
300267
assertThat(struct1.getErrorDetail()).isNull();
301268
assertThat(struct1.getStruct()).isEqualTo(
302269
new RouteMatch(
303-
new PathMatcher(null, "/", null), Collections.<HeaderMatcher>emptyList(), null));
270+
PathMatcher.fromPrefix("/", false), Collections.<HeaderMatcher>emptyList(), null));
271+
272+
proto1 = proto1.toBuilder().setCaseSensitive(BoolValue.newBuilder().setValue(true)).build();
273+
struct1 = Route.convertEnvoyProtoRouteMatch(proto1);
274+
assertThat(struct1.getStruct()).isEqualTo(
275+
new RouteMatch(
276+
PathMatcher.fromPrefix("/", true), Collections.<HeaderMatcher>emptyList(), null));
304277

305278
// path_specifier = path
306279
io.envoyproxy.envoy.config.route.v3.RouteMatch proto2 =
@@ -311,7 +284,14 @@ public void convertRouteMatch_pathMatching() {
311284
assertThat(struct2.getErrorDetail()).isNull();
312285
assertThat(struct2.getStruct()).isEqualTo(
313286
new RouteMatch(
314-
new PathMatcher("/service/method", null, null),
287+
PathMatcher.fromPath("/service/method", false),
288+
Collections.<HeaderMatcher>emptyList(), null));
289+
290+
proto2 = proto2.toBuilder().setCaseSensitive(BoolValue.newBuilder().setValue(true)).build();
291+
struct2 = Route.convertEnvoyProtoRouteMatch(proto2);
292+
assertThat(struct2.getStruct()).isEqualTo(
293+
new RouteMatch(
294+
PathMatcher.fromPath("/service/method", true),
315295
Collections.<HeaderMatcher>emptyList(), null));
316296

317297
// path_specifier = safe_regex
@@ -323,18 +303,9 @@ public void convertRouteMatch_pathMatching() {
323303
assertThat(struct4.getErrorDetail()).isNull();
324304
assertThat(struct4.getStruct()).isEqualTo(
325305
new RouteMatch(
326-
new PathMatcher(null, null, Pattern.compile(".")),
306+
PathMatcher.fromRegEx(Pattern.compile(".")),
327307
Collections.<HeaderMatcher>emptyList(), null));
328308

329-
// case_sensitive = false
330-
io.envoyproxy.envoy.config.route.v3.RouteMatch proto5 =
331-
io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder()
332-
.setCaseSensitive(BoolValue.newBuilder().setValue(false))
333-
.build();
334-
StructOrError<RouteMatch> struct5 = Route.convertEnvoyProtoRouteMatch(proto5);
335-
assertThat(struct5.getErrorDetail()).isNotNull();
336-
assertThat(struct5.getStruct()).isNull();
337-
338309
// query_parameters is set
339310
io.envoyproxy.envoy.config.route.v3.RouteMatch proto6 =
340311
io.envoyproxy.envoy.config.route.v3.RouteMatch.newBuilder()
@@ -370,7 +341,7 @@ public void convertRouteMatch_withHeaderMatching() {
370341
assertThat(struct.getStruct())
371342
.isEqualTo(
372343
new RouteMatch(
373-
new PathMatcher(null, "", null),
344+
PathMatcher.fromPrefix("", false),
374345
Arrays.asList(
375346
new HeaderMatcher(":scheme", null, null, null, null, "http", null, false),
376347
new HeaderMatcher(":method", "PUT", null, null, null, null, null, false)),
@@ -394,7 +365,7 @@ public void convertRouteMatch_withRuntimeFraction() {
394365
assertThat(struct.getStruct())
395366
.isEqualTo(
396367
new RouteMatch(
397-
new PathMatcher(null, "", null), Collections.<HeaderMatcher>emptyList(),
368+
PathMatcher.fromPrefix( "", false), Collections.<HeaderMatcher>emptyList(),
398369
new FractionMatcher(30, 100)));
399370
}
400371

0 commit comments

Comments
 (0)