Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion xds/src/main/java/io/grpc/xds/internal/MatcherParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,46 @@ public static Matchers.StringMatcher parseStringMatcher(
return Matchers.StringMatcher.forSafeRegEx(
Pattern.compile(proto.getSafeRegex().getRegex()));
case CONTAINS:
return Matchers.StringMatcher.forContains(proto.getContains());
return Matchers.StringMatcher.forContains(proto.getContains(), proto.getIgnoreCase());
case MATCHPATTERN_NOT_SET:
default:
throw new IllegalArgumentException(
"Unknown StringMatcher match pattern: " + proto.getMatchPatternCase());
}
}

/** Translate StringMatcher xDS proto to internal StringMatcher. */
public static Matchers.StringMatcher parseStringMatcher(
com.github.xds.type.matcher.v3.StringMatcher proto) {
switch (proto.getMatchPatternCase()) {
case EXACT:
return Matchers.StringMatcher.forExact(proto.getExact(), proto.getIgnoreCase());
case PREFIX:
return Matchers.StringMatcher.forPrefix(
checkNonEmpty(proto.getPrefix(), "prefix"), proto.getIgnoreCase());
case SUFFIX:
return Matchers.StringMatcher.forSuffix(
checkNonEmpty(proto.getSuffix(), "suffix"), proto.getIgnoreCase());
case CONTAINS:
return Matchers.StringMatcher.forContains(
checkNonEmpty(proto.getContains(), "contains"), proto.getIgnoreCase());
case SAFE_REGEX:
String regex = checkNonEmpty(proto.getSafeRegex().getRegex(), "regex");
return Matchers.StringMatcher.forSafeRegEx(Pattern.compile(regex));
default:
throw new IllegalArgumentException(
"Unknown StringMatcher match pattern: " + proto.getMatchPatternCase());
}
}

private static String checkNonEmpty(String value, String name) {
if (value.isEmpty()) {
throw new IllegalArgumentException("StringMatcher " + name
+ " (match_pattern) must be non-empty");
}
return value;
}

/** Translates envoy proto FractionalPercent to internal FractionMatcher. */
public static Matchers.FractionMatcher parseFractionMatcher(
io.envoyproxy.envoy.type.v3.FractionalPercent proto) {
Expand Down
13 changes: 10 additions & 3 deletions xds/src/main/java/io/grpc/xds/internal/Matchers.java
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,15 @@ public static StringMatcher forSafeRegEx(Pattern regEx) {
}

/** The input string should contain this substring. */
public static StringMatcher forContains(String contains) {
public static StringMatcher forContains(String contains, boolean ignoreCase) {
checkNotNull(contains, "contains");
return StringMatcher.create(null, null, null, null, contains,
false/* doesn't matter */);
ignoreCase);
}

/** The input string should contain this substring. */
public static StringMatcher forContains(String contains) {
return forContains(contains, false);
}

/** Returns the matching result for this string. */
Expand All @@ -281,7 +286,9 @@ public boolean matches(String args) {
? args.toLowerCase(Locale.ROOT).endsWith(suffix().toLowerCase(Locale.ROOT))
: args.endsWith(suffix());
} else if (contains() != null) {
return args.contains(contains());
return ignoreCase()
? args.toLowerCase(Locale.ROOT).contains(contains().toLowerCase(Locale.ROOT))
: args.contains(contains());
}
return regEx().matches(args);
}
Expand Down
70 changes: 70 additions & 0 deletions xds/src/main/java/io/grpc/xds/internal/matcher/CelMatcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2026 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc.xds.internal.matcher;

import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.types.SimpleType;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelRuntime;
import dev.cel.runtime.CelVariableResolver;

/**
* Executes compiled CEL expressions.
*/
public final class CelMatcher {
private final CelRuntime.Program program;

private CelMatcher(CelRuntime.Program program) {
this.program = program;
}

/**
* Compiles the AST into a CelMatcher.
* Throws an Exception if evaluation fails during compilation setup.
*/
public static CelMatcher compile(CelAbstractSyntaxTree ast)
throws CelEvaluationException {
// CelEvaluationException -> inside cel-runtime -> Allowed in production signatures
// CelValidationException -> inside cel-compiler -> Forbidden in production signatures
if (ast.getResultType() != SimpleType.BOOL) {
throw new IllegalArgumentException(
"CEL expression must evaluate to boolean, got: " + ast.getResultType());
}
CelCommon.checkAllowedReferences(ast);
CelRuntime.Program program = CelCommon.RUNTIME.createProgram(ast);
return new CelMatcher(program);
}

/**
* Evaluates the CEL expression against the input activation.
*/
public boolean match(Object input) throws CelEvaluationException {
Object result;
if (input instanceof CelVariableResolver) {
result = program.eval((CelVariableResolver) input);
} else {
throw new CelEvaluationException(
"Unsupported input type for CEL evaluation: " + input.getClass().getName());
}

if (result instanceof Boolean) {
return (Boolean) result;
}
throw new CelEvaluationException(
"CEL expression must evaluate to boolean, got: " + result.getClass().getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2026 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc.xds.internal.matcher;

import com.github.xds.core.v3.TypedExtensionConfig;
import com.github.xds.type.v3.CelExpression;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelProtoAbstractSyntaxTree;
import dev.cel.runtime.CelEvaluationException;

/**
* Matcher for CEL expressions handling xDS CEL Matcher extension.
*/
final class CelStateMatcher implements Matcher {
private final CelMatcher compiledEndpoint;
static final String TYPE_URL = "type.googleapis.com/xds.type.matcher.v3.CelMatcher";

CelStateMatcher(CelMatcher compiledEndpoint) {
this.compiledEndpoint = compiledEndpoint;
}

@Override
public boolean match(Object value) {
try {
return compiledEndpoint.match(value);
} catch (CelEvaluationException e) {
return false;
}
}

@Override
public Class<?> inputType() {
return GrpcCelEnvironment.class;
}

static final class Provider implements MatcherProvider {
@Override
public CelStateMatcher getMatcher(TypedExtensionConfig config) {
try {
com.github.xds.type.matcher.v3.CelMatcher celProto = config.getTypedConfig()
.unpack(com.github.xds.type.matcher.v3.CelMatcher.class);
if (!celProto.hasExprMatch()) {
throw new IllegalArgumentException("CelMatcher must have expr_match");
}
CelExpression expr = celProto.getExprMatch();
if (!expr.hasCelExprChecked()) {
throw new IllegalArgumentException("CelMatcher must have cel_expr_checked");
}
CelAbstractSyntaxTree ast =
CelProtoAbstractSyntaxTree.fromCheckedExpr(
expr.getCelExprChecked()).getAst();
CelMatcher compiled = CelMatcher.compile(ast);

return new CelStateMatcher(compiled);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid CelMatcher config", e);
}
}

@Override
public String typeUrl() {
return TYPE_URL;
}
}
}
109 changes: 109 additions & 0 deletions xds/src/main/java/io/grpc/xds/internal/matcher/HeaderMatchInput.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2026 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc.xds.internal.matcher;

import static com.google.common.base.Preconditions.checkNotNull;

import com.github.xds.core.v3.TypedExtensionConfig;
import com.google.protobuf.InvalidProtocolBufferException;
import io.envoyproxy.envoy.type.matcher.v3.HttpRequestHeaderMatchInput;
import io.grpc.Metadata;
import java.util.Locale;

/**
* MatchInput for extracting HTTP headers.
*/
final class HeaderMatchInput implements MatchInput {
private final String headerName;
static final String TYPE_URL =
"type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput";

HeaderMatchInput(String headerName) {
this.headerName = checkNotNull(headerName, "headerName");
if (headerName.isEmpty() || headerName.length() >= 16384) {
throw new IllegalArgumentException(
"Header name length must be in range [1, 16384): " + headerName.length());
}
if (!headerName.equals(headerName.toLowerCase(Locale.ROOT))) {
throw new IllegalArgumentException("Header name must be lowercase: " + headerName);
}
try {
if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
Metadata.Key.of(headerName, Metadata.BINARY_BYTE_MARSHALLER);
} else {
Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER);
}
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid header name: " + headerName, e);
}
}

@Override
public String apply(MatchContext context) {
if ("te".equals(headerName)) {
return null;
}
if (headerName.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
Iterable<byte[]> values = context.getMetadata().getAll(
Metadata.Key.of(headerName, Metadata.BINARY_BYTE_MARSHALLER));
if (values == null) {
return null;
}
StringBuilder sb = new StringBuilder();
boolean first = true;
for (byte[] value : values) {
if (!first) {
sb.append(",");
}
first = false;
sb.append(com.google.common.io.BaseEncoding.base64().encode(value));
}
return sb.toString();
}
Metadata metadata = context.getMetadata();
Iterable<String> values = metadata.getAll(
Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER));
if (values == null) {
return null;
}
return String.join(",", values);
}

@Override
public Class<?> outputType() {
return String.class;
}

static final class Provider implements MatchInputProvider {
@Override
public HeaderMatchInput getInput(TypedExtensionConfig config) {
try {
HttpRequestHeaderMatchInput proto = config.getTypedConfig()
.unpack(HttpRequestHeaderMatchInput.class);
return new HeaderMatchInput(proto.getHeaderName());
} catch (InvalidProtocolBufferException e) {
throw new IllegalArgumentException(
"Invalid input config: " + config.getTypedConfig().getTypeUrl(), e);
}
}

@Override
public String typeUrl() {
return TYPE_URL;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2026 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc.xds.internal.matcher;

import com.github.xds.core.v3.TypedExtensionConfig;

/**
* MatchInput for extracting CEL environment from HTTP attributes.
*/
final class HttpAttributesCelMatchInput implements MatchInput {
static final HttpAttributesCelMatchInput INSTANCE = new HttpAttributesCelMatchInput();
static final String TYPE_URL =
"type.googleapis.com/xds.type.matcher.v3.HttpAttributesCelMatchInput";

private HttpAttributesCelMatchInput() {}

@Override
public GrpcCelEnvironment apply(MatchContext context) {
return new GrpcCelEnvironment(context);
}

@Override
public Class<?> outputType() {
return GrpcCelEnvironment.class;
}

static final class Provider implements MatchInputProvider {
@Override
public HttpAttributesCelMatchInput getInput(TypedExtensionConfig config) {
return INSTANCE;
}

@Override
public String typeUrl() {
return TYPE_URL;
}
}
}
Loading
Loading