Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2024 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.headermutations;

import io.grpc.Status;
import io.grpc.StatusException;

/**
* Exception thrown when a header mutation is disallowed.
*/
public final class HeaderMutationDisallowedException extends StatusException {

private static final long serialVersionUID = 1L;

public HeaderMutationDisallowedException(String message) {
super(Status.INTERNAL.withDescription(message));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2025 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.headermutations;

import com.google.common.collect.ImmutableList;
import io.grpc.xds.internal.grpcservice.HeaderValueValidationUtils;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Predicate;

/**
* The HeaderMutationFilter class is responsible for filtering header mutations based on a given set
* of rules.
*/
public class HeaderMutationFilter {
private final Optional<HeaderMutationRulesConfig> mutationRules;



public HeaderMutationFilter(Optional<HeaderMutationRulesConfig> mutationRules) {
this.mutationRules = mutationRules;
}

/**
* Filters the given header mutations based on the configured rules and returns the allowed
* mutations.
*
* @param mutations The header mutations to filter
* @return The allowed header mutations.
* @throws HeaderMutationDisallowedException if a disallowed mutation is encountered and the rules
* specify that this should be an error.
*/
public HeaderMutations filter(HeaderMutations mutations)
throws HeaderMutationDisallowedException {
ImmutableList<HeaderValueOption> allowedHeaders =
filterCollection(mutations.headers(), this::isDisallowed, this::isHeaderMutationAllowed);
ImmutableList<String> allowedHeadersToRemove =
filterCollection(mutations.headersToRemove(), this::isDisallowed,
this::isHeaderMutationAllowed);
return HeaderMutations.create(allowedHeaders, allowedHeadersToRemove);
}

/**
* A generic helper to filter a collection based on a predicate.
*/
private <T> ImmutableList<T> filterCollection(Collection<T> items,
Predicate<T> isIgnoredPredicate, Predicate<T> isAllowedPredicate)
throws HeaderMutationDisallowedException {
ImmutableList.Builder<T> allowed = ImmutableList.builder();
for (T item : items) {
boolean isIgnored = isIgnoredPredicate.test(item);
boolean isAllowed = isAllowedPredicate.test(item);

// TODO(sauravzg): The specification is ambiguous regarding whether system headers
// should be silently ignored or trigger an error when disallowIsError is enabled.
// We default to triggering errors matching Envoy's implementation.
// Ref: https://github.com/grpc/proposal/pull/481#discussion_r3124453674
if (!isIgnored && isAllowed) {
allowed.add(item);
} else if (disallowIsError()) {
throw new HeaderMutationDisallowedException("Header mutation disallowed");
Comment thread
sauravzg marked this conversation as resolved.
}
}
return allowed.build();
}
Comment thread
sauravzg marked this conversation as resolved.

private boolean isDisallowed(String key) {
return HeaderValueValidationUtils.isDisallowed(key);
}

private boolean isDisallowed(HeaderValueOption option) {
return HeaderValueValidationUtils.isDisallowed(option.header());
}

private boolean isHeaderMutationAllowed(HeaderValueOption option) {
return isHeaderMutationAllowed(option.header().key());
}

private boolean isHeaderMutationAllowed(String headerName) {
return mutationRules.map(rules -> isHeaderMutationAllowed(headerName, rules))
.orElse(true);
}

private boolean isHeaderMutationAllowed(String headerName,
HeaderMutationRulesConfig rules) {
if (rules.disallowExpression().isPresent()
&& rules.disallowExpression().get().matcher(headerName).matches()) {
return false;
}
if (rules.allowExpression().isPresent()
&& rules.allowExpression().get().matcher(headerName).matches()) {
return true;
}
return !rules.disallowAll();
Comment thread
kannanjgithub marked this conversation as resolved.
}

private boolean disallowIsError() {
return mutationRules.map(HeaderMutationRulesConfig::disallowIsError).orElse(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2025 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.headermutations;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;

/** A collection of header mutations. */
@AutoValue
public abstract class HeaderMutations {

public static HeaderMutations create(ImmutableList<HeaderValueOption> headers,
ImmutableList<String> headersToRemove) {
return new AutoValue_HeaderMutations(headers, headersToRemove);
}

public abstract ImmutableList<HeaderValueOption> headers();

public abstract ImmutableList<String> headersToRemove();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2025 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.headermutations;


import io.grpc.Metadata;
import io.grpc.xds.internal.grpcservice.HeaderValue;
import io.grpc.xds.internal.headermutations.HeaderValueOption.HeaderAppendAction;
import java.util.logging.Logger;

/**
* The HeaderMutator provides methods to apply header mutations to a given set of headers based on a
* given set of rules.
*/
public class HeaderMutator {

private static final Logger logger = Logger.getLogger(HeaderMutator.class.getName());

/**
* Creates a new instance of {@code HeaderMutator}.
*/
public static HeaderMutator create() {
return new HeaderMutator();
}

HeaderMutator() {}

/**
* Applies the given header mutations to the provided metadata headers.
*
* @param mutations The header mutations to apply.
* @param headers The metadata headers to which the mutations will be applied.
*/
public void applyMutations(final HeaderMutations mutations, Metadata headers) {
// TODO(sauravzg): The specification is not clear on order of header removals and additions.
// in case of conflicts. Copying the order from Envoy here, which does removals at the end.
applyHeaderUpdates(mutations.headers(), headers);
for (String headerToRemove : mutations.headersToRemove()) {
Metadata.Key<?> key = headerToRemove.endsWith(Metadata.BINARY_HEADER_SUFFIX)
? Metadata.Key.of(headerToRemove, Metadata.BINARY_BYTE_MARSHALLER)
: Metadata.Key.of(headerToRemove, Metadata.ASCII_STRING_MARSHALLER);
Comment thread
sauravzg marked this conversation as resolved.
Comment thread
sauravzg marked this conversation as resolved.
Comment thread
sauravzg marked this conversation as resolved.
headers.discardAll(key);
}
Comment thread
sauravzg marked this conversation as resolved.
}
Comment thread
sauravzg marked this conversation as resolved.

private void applyHeaderUpdates(final Iterable<HeaderValueOption> headerOptions,
Metadata headers) {
for (HeaderValueOption headerOption : headerOptions) {
updateHeader(headerOption, headers);
}
}

private void updateHeader(final HeaderValueOption option, Metadata mutableHeaders) {
HeaderValue header = option.header();
HeaderAppendAction action = option.appendAction();
boolean keepEmptyValue = option.keepEmptyValue();
Comment thread
sauravzg marked this conversation as resolved.

if (header.key().endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
if (header.rawValue().isPresent()) {
byte[] value = header.rawValue().get().toByteArray();
if (value.length > 0 || keepEmptyValue) {
updateHeader(action, Metadata.Key.of(header.key(), Metadata.BINARY_BYTE_MARSHALLER),
value, mutableHeaders);
}
} else {
logger.fine("Missing binary rawValue for header: " + header.key());
}
} else {
Comment thread
sauravzg marked this conversation as resolved.
if (header.value().isPresent()) {
String value = header.value().get();
if (!value.isEmpty() || keepEmptyValue) {
updateHeader(action, Metadata.Key.of(header.key(), Metadata.ASCII_STRING_MARSHALLER),
value, mutableHeaders);
}
} else {
logger.fine("Missing value for header: " + header.key());
}
}
}

private <T> void updateHeader(final HeaderAppendAction action, final Metadata.Key<T> key,
final T value, Metadata mutableHeaders) {
switch (action) {
case APPEND_IF_EXISTS_OR_ADD:
mutableHeaders.put(key, value);
break;
case ADD_IF_ABSENT:
if (!mutableHeaders.containsKey(key)) {
mutableHeaders.put(key, value);
}
break;
case OVERWRITE_IF_EXISTS_OR_ADD:
mutableHeaders.discardAll(key);
mutableHeaders.put(key, value);
break;
case OVERWRITE_IF_EXISTS:
if (mutableHeaders.containsKey(key)) {
mutableHeaders.discardAll(key);
mutableHeaders.put(key, value);
}
break;

default:
// Should be unreachable unless there's a proto schema mismatch.
logger.fine("Unknown HeaderAppendAction: " + action);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.headermutations;

import com.google.auto.value.AutoValue;
import io.grpc.xds.internal.grpcservice.HeaderValue;

/**
* Represents a header option to be appended or mutated as part of xDS configuration.
* Avoids direct dependency on Envoy's proto objects.
Comment thread
kannanjgithub marked this conversation as resolved.
*/
@AutoValue
public abstract class HeaderValueOption {

public static HeaderValueOption create(
HeaderValue header, HeaderAppendAction appendAction, boolean keepEmptyValue) {
return new AutoValue_HeaderValueOption(header, appendAction, keepEmptyValue);
}

public abstract HeaderValue header();

public abstract HeaderAppendAction appendAction();

public abstract boolean keepEmptyValue();

/**
* Defines the action to take when appending headers.
* Mirrors io.envoyproxy.envoy.config.core.v3.HeaderValueOption.HeaderAppendAction.
*/
public enum HeaderAppendAction {
APPEND_IF_EXISTS_OR_ADD,
ADD_IF_ABSENT,
OVERWRITE_IF_EXISTS_OR_ADD,
OVERWRITE_IF_EXISTS
}
}
Loading
Loading