Skip to content

Commit 6329b7c

Browse files
committed
Merge branch 'sauravzg-feat-header-mutations' into ext-proc
2 parents c411d47 + d999d4b commit 6329b7c

9 files changed

Lines changed: 972 additions & 0 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2024 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.internal.headermutations;
18+
19+
import io.grpc.Status;
20+
import io.grpc.StatusException;
21+
22+
/**
23+
* Exception thrown when a header mutation is disallowed.
24+
*/
25+
public final class HeaderMutationDisallowedException extends StatusException {
26+
27+
private static final long serialVersionUID = 1L;
28+
29+
public HeaderMutationDisallowedException(String message) {
30+
super(Status.INTERNAL.withDescription(message));
31+
}
32+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2025 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.internal.headermutations;
18+
19+
import com.google.common.collect.ImmutableList;
20+
import io.grpc.xds.internal.grpcservice.HeaderValueValidationUtils;
21+
import java.util.Collection;
22+
import java.util.Optional;
23+
import java.util.function.Predicate;
24+
25+
/**
26+
* The HeaderMutationFilter class is responsible for filtering header mutations based on a given set
27+
* of rules.
28+
*/
29+
public class HeaderMutationFilter {
30+
private final Optional<HeaderMutationRulesConfig> mutationRules;
31+
32+
33+
34+
public HeaderMutationFilter(Optional<HeaderMutationRulesConfig> mutationRules) {
35+
this.mutationRules = mutationRules;
36+
}
37+
38+
/**
39+
* Filters the given header mutations based on the configured rules and returns the allowed
40+
* mutations.
41+
*
42+
* @param mutations The header mutations to filter
43+
* @return The allowed header mutations.
44+
* @throws HeaderMutationDisallowedException if a disallowed mutation is encountered and the rules
45+
* specify that this should be an error.
46+
*/
47+
public HeaderMutations filter(HeaderMutations mutations)
48+
throws HeaderMutationDisallowedException {
49+
ImmutableList<HeaderValueOption> allowedHeaders =
50+
filterCollection(mutations.headers(), this::isDisallowed, this::isHeaderMutationAllowed);
51+
ImmutableList<String> allowedHeadersToRemove =
52+
filterCollection(mutations.headersToRemove(), this::isDisallowed,
53+
this::isHeaderMutationAllowed);
54+
return HeaderMutations.create(allowedHeaders, allowedHeadersToRemove);
55+
}
56+
57+
/**
58+
* A generic helper to filter a collection based on a predicate.
59+
*/
60+
private <T> ImmutableList<T> filterCollection(Collection<T> items,
61+
Predicate<T> isIgnoredPredicate, Predicate<T> isAllowedPredicate)
62+
throws HeaderMutationDisallowedException {
63+
ImmutableList.Builder<T> allowed = ImmutableList.builder();
64+
for (T item : items) {
65+
if (isIgnoredPredicate.test(item)) {
66+
continue;
67+
}
68+
if (isAllowedPredicate.test(item)) {
69+
allowed.add(item);
70+
} else if (disallowIsError()) {
71+
throw new HeaderMutationDisallowedException("Header mutation disallowed");
72+
}
73+
}
74+
return allowed.build();
75+
}
76+
77+
private boolean isDisallowed(String key) {
78+
return HeaderValueValidationUtils.isDisallowed(key);
79+
}
80+
81+
private boolean isDisallowed(HeaderValueOption option) {
82+
return HeaderValueValidationUtils.isDisallowed(option.header());
83+
}
84+
85+
private boolean isHeaderMutationAllowed(HeaderValueOption option) {
86+
return isHeaderMutationAllowed(option.header().key());
87+
}
88+
89+
private boolean isHeaderMutationAllowed(String headerName) {
90+
return mutationRules.map(rules -> isHeaderMutationAllowed(headerName, rules))
91+
.orElse(true);
92+
}
93+
94+
private boolean isHeaderMutationAllowed(String headerName,
95+
HeaderMutationRulesConfig rules) {
96+
if (rules.disallowExpression().isPresent()
97+
&& rules.disallowExpression().get().matcher(headerName).matches()) {
98+
return false;
99+
}
100+
if (rules.allowExpression().isPresent()) {
101+
return rules.allowExpression().get().matcher(headerName).matches();
102+
}
103+
return !rules.disallowAll();
104+
}
105+
106+
private boolean disallowIsError() {
107+
return mutationRules.map(HeaderMutationRulesConfig::disallowIsError).orElse(false);
108+
}
109+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2025 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.internal.headermutations;
18+
19+
import com.google.auto.value.AutoValue;
20+
import com.google.common.collect.ImmutableList;
21+
22+
/** A collection of header mutations. */
23+
@AutoValue
24+
public abstract class HeaderMutations {
25+
26+
public static HeaderMutations create(ImmutableList<HeaderValueOption> headers,
27+
ImmutableList<String> headersToRemove) {
28+
return new AutoValue_HeaderMutations(headers, headersToRemove);
29+
}
30+
31+
public abstract ImmutableList<HeaderValueOption> headers();
32+
33+
public abstract ImmutableList<String> headersToRemove();
34+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright 2025 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.internal.headermutations;
18+
19+
20+
import io.grpc.Metadata;
21+
import io.grpc.xds.internal.grpcservice.HeaderValue;
22+
import io.grpc.xds.internal.headermutations.HeaderValueOption.HeaderAppendAction;
23+
import java.util.logging.Logger;
24+
25+
/**
26+
* The HeaderMutator class is an implementation of the HeaderMutator interface. It provides methods
27+
* to apply header mutations to a given set of headers based on a given set of rules.
28+
*/
29+
public class HeaderMutator {
30+
31+
private static final Logger logger = Logger.getLogger(HeaderMutator.class.getName());
32+
33+
/**
34+
* Creates a new instance of {@code HeaderMutator}.
35+
*/
36+
public static HeaderMutator create() {
37+
return new HeaderMutator();
38+
}
39+
40+
HeaderMutator() {}
41+
42+
/**
43+
* Applies the given header mutations to the provided metadata headers.
44+
*
45+
* @param mutations The header mutations to apply.
46+
* @param headers The metadata headers to which the mutations will be applied.
47+
*/
48+
public void applyMutations(final HeaderMutations mutations, Metadata headers) {
49+
// TODO(sauravzg): The specification is not clear on order of header removals and additions.
50+
// in case of conflicts. Copying the order from Envoy here, which does removals at the end.
51+
applyHeaderUpdates(mutations.headers(), headers);
52+
for (String headerToRemove : mutations.headersToRemove()) {
53+
Metadata.Key<?> key = headerToRemove.endsWith(Metadata.BINARY_HEADER_SUFFIX)
54+
? Metadata.Key.of(headerToRemove, Metadata.BINARY_BYTE_MARSHALLER)
55+
: Metadata.Key.of(headerToRemove, Metadata.ASCII_STRING_MARSHALLER);
56+
headers.discardAll(key);
57+
}
58+
}
59+
60+
private void applyHeaderUpdates(final Iterable<HeaderValueOption> headerOptions,
61+
Metadata headers) {
62+
for (HeaderValueOption headerOption : headerOptions) {
63+
updateHeader(headerOption, headers);
64+
}
65+
}
66+
67+
private void updateHeader(final HeaderValueOption option, Metadata mutableHeaders) {
68+
HeaderValue header = option.header();
69+
HeaderAppendAction action = option.appendAction();
70+
boolean keepEmptyValue = option.keepEmptyValue();
71+
72+
if (header.key().endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
73+
if (header.rawValue().isPresent()) {
74+
updateHeader(action, Metadata.Key.of(header.key(), Metadata.BINARY_BYTE_MARSHALLER),
75+
header.rawValue().get().toByteArray(), mutableHeaders, keepEmptyValue);
76+
} else {
77+
logger.fine("Missing binary rawValue for header: " + header.key());
78+
}
79+
} else {
80+
if (header.value().isPresent()) {
81+
updateHeader(action, Metadata.Key.of(header.key(), Metadata.ASCII_STRING_MARSHALLER),
82+
header.value().get(), mutableHeaders, keepEmptyValue);
83+
} else {
84+
logger.fine("Missing value for header: " + header.key());
85+
}
86+
}
87+
}
88+
89+
private <T> void updateHeader(final HeaderAppendAction action, final Metadata.Key<T> key,
90+
final T value, Metadata mutableHeaders, boolean keepEmptyValue) {
91+
switch (action) {
92+
case APPEND_IF_EXISTS_OR_ADD:
93+
mutableHeaders.put(key, value);
94+
break;
95+
case ADD_IF_ABSENT:
96+
if (!mutableHeaders.containsKey(key)) {
97+
mutableHeaders.put(key, value);
98+
}
99+
break;
100+
case OVERWRITE_IF_EXISTS_OR_ADD:
101+
mutableHeaders.discardAll(key);
102+
mutableHeaders.put(key, value);
103+
break;
104+
case OVERWRITE_IF_EXISTS:
105+
if (mutableHeaders.containsKey(key)) {
106+
mutableHeaders.discardAll(key);
107+
mutableHeaders.put(key, value);
108+
}
109+
break;
110+
111+
default:
112+
// Should be unreachable unless there's a proto schema mismatch.
113+
logger.fine("Unknown HeaderAppendAction: " + action);
114+
}
115+
116+
if (!keepEmptyValue) {
117+
checkAndRemoveEmpty(key, mutableHeaders);
118+
}
119+
}
120+
121+
private <T> void checkAndRemoveEmpty(Metadata.Key<T> key, Metadata headers) {
122+
Iterable<T> values = headers.getAll(key);
123+
if (values == null) {
124+
return;
125+
}
126+
boolean allEmpty = true;
127+
for (T val : values) {
128+
if (val instanceof String) {
129+
if (!((String) val).isEmpty()) {
130+
allEmpty = false;
131+
break;
132+
}
133+
} else if (val instanceof byte[]) {
134+
if (((byte[]) val).length > 0) {
135+
allEmpty = false;
136+
break;
137+
}
138+
}
139+
}
140+
if (allEmpty) {
141+
headers.discardAll(key);
142+
}
143+
}
144+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2026 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.internal.headermutations;
18+
19+
import com.google.auto.value.AutoValue;
20+
import io.grpc.xds.internal.grpcservice.HeaderValue;
21+
22+
/**
23+
* Represents a header option to be appended or mutated as part of xDS configuration.
24+
* Avoids direct dependency on Envoy's proto objects.
25+
*/
26+
@AutoValue
27+
public abstract class HeaderValueOption {
28+
29+
public static HeaderValueOption create(
30+
HeaderValue header, HeaderAppendAction appendAction, boolean keepEmptyValue) {
31+
return new AutoValue_HeaderValueOption(header, appendAction, keepEmptyValue);
32+
}
33+
34+
public abstract HeaderValue header();
35+
36+
public abstract HeaderAppendAction appendAction();
37+
38+
public abstract boolean keepEmptyValue();
39+
40+
/**
41+
* Defines the action to take when appending headers.
42+
* Mirrors io.envoyproxy.envoy.config.core.v3.HeaderValueOption.HeaderAppendAction.
43+
*/
44+
public enum HeaderAppendAction {
45+
APPEND_IF_EXISTS_OR_ADD,
46+
ADD_IF_ABSENT,
47+
OVERWRITE_IF_EXISTS_OR_ADD,
48+
OVERWRITE_IF_EXISTS
49+
}
50+
}

0 commit comments

Comments
 (0)