Skip to content
Open
Show file tree
Hide file tree
Changes from 19 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
1 change: 1 addition & 0 deletions internal/envconfig/xds.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ var (
// the client side. For more details, see:
// https://github.com/grpc/proposal/blob/master/A93-xds-ext-proc.md
XDSClientExtProcEnabled = boolFromEnv("GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT", false)

// GCPAuthenticationFilterEnabled enables the xDS GCP Authentication
// filter. For more details, see:
// https://github.com/grpc/proposal/blob/master/A83-xds-gcp-authn-filter.md
Expand Down
47 changes: 47 additions & 0 deletions internal/optional/optional.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
*
* Copyright 2026 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 optional implements a generic optional type.
package optional

// Optional represents an optional value of type T. The zero value is usable and
// indicates that no value is set. This type is not safe for concurrent access.
type Optional[T any] struct {
val T
set bool
}

// New creates a new Optional type with the provided value.
func New[T any](value T) Optional[T] {
return Optional[T]{
val: value,
set: true,
}
}

// Value returns the underlying value and a boolean indicating if the value is
// set. If the value is not set, it returns the zero value of T and false.
func (o *Optional[T]) Value() (T, bool) {
return o.val, o.set
}

// SetValue updates or adds the value to Optional.
func (o *Optional[T]) SetValue(v T) {
o.val = v
o.set = true
}
130 changes: 130 additions & 0 deletions internal/optional/optional_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
*
* Copyright 2026 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 optional_test

import (
"slices"
"testing"

"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/optional"
)

type s struct {
grpctest.Tester
}

func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}

// TestOptionalInt tests the scenario of using integer optional values and
// verifies that zero value, constructors, and mutation methods return correct
// outputs.
func (s) TestOptionalInt(t *testing.T) {
var opt optional.Optional[int]

// Test unset value.
if v, set := opt.Value(); set || v != 0 {
t.Fatalf("Zero-value Option[int] = (%v, %v); want (0, false)", v, set)
}

opt = optional.New(42)
if v, set := opt.Value(); !set || v != 42 {
t.Fatalf("NewValue(42) = (%v, %v); want (42, true)", v, set)
}

opt.SetValue(100)
if v, set := opt.Value(); !set || v != 100 {
t.Fatalf("Set(100) = (%v, %v); want (100, true)", v, set)
}
}

// TestOptionalString tests the scenario of using string optional values and
// verifies that zero value, constructors, and mutation methods return correct
// outputs.
func (s) TestOptionalString(t *testing.T) {
var opt optional.Optional[string]

// Test unset value.
if v, set := opt.Value(); set || v != "" {
t.Fatalf("Zero-value Option[string] = (%q, %v); want (%q, false)", v, set, "")
}

wantString := "test-string"
opt = optional.New(wantString)
if v, set := opt.Value(); !set || v != wantString {
t.Fatalf("NewValue(%q) = (%q, %v); want (%q, true)", wantString, v, set, wantString)
}

wantStringNew := "world"
opt.SetValue(wantStringNew)
if v, set := opt.Value(); !set || v != wantStringNew {
t.Fatalf("Set(%q) = (%q, %v); want (%q, true)", wantStringNew, v, set, wantStringNew)
}
}

// TestOptionalStruct tests the scenario of using a custom struct optional value
// and verifies that custom struct field values are preserved, modified, and
// cleared correctly.
func (s) TestOptionalStruct(t *testing.T) {
type testStruct struct {
name string
age int
}

var opt optional.Optional[testStruct]
if v, set := opt.Value(); set || v != (testStruct{}) {
t.Fatalf("Zero-value Option[struct] = (%v, %v); want (empty, false)", v, set)
}

want := testStruct{name: "Alice", age: 30}
opt = optional.New(want)
if v, set := opt.Value(); !set || v != want {
t.Fatalf("NewValue(%v) = (%v, %v); want (%v, true)", want, v, set, want)
}

want2 := testStruct{name: "Bob", age: 40}
opt.SetValue(want2)
if v, set := opt.Value(); !set || v != want2 {
t.Fatalf("Set(%v) = (%v, %v); want (%v, true)", want2, v, set, want2)
}
}

// TestOptionalSlice tests the scenario of using a slice optional value and
// verifies that zero value, constructors, and mutation methods return correct
// outputs.
func (s) TestOptionalSlice(t *testing.T) {
var opt optional.Optional[[]int]
if v, set := opt.Value(); set || v != nil {
t.Fatalf("Zero-value Option[[]int] = (%v, %v); want (nil, false)", v, set)
}

want := []int{1, 2, 3}
opt = optional.New(want)
if v, set := opt.Value(); !set || !slices.Equal(v, want) {
t.Fatalf("NewValue(%v) = (%v, %v); want (%v, true)", want, v, set, want)
}

want2 := []int{4, 5, 6}
opt.SetValue(want2)
if v, set := opt.Value(); !set || !slices.Equal(v, want2) {
t.Fatalf("Set(%v) = (%v, %v); want (%v, true)", &want2, v, set, &want2)
}
}
88 changes: 88 additions & 0 deletions internal/xds/httpfilter/extconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
*
* Copyright 2026 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 httpfilter

import (
"fmt"
"regexp"

"google.golang.org/grpc/internal/xds/matcher"

v3mutationpb "github.com/envoyproxy/go-control-plane/envoy/config/common/mutation_rules/v3"
v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
)

// HeaderMutationRules specifies the rules for what modifications an external
// processing server may make to headers sent on the data plane RPC.
type HeaderMutationRules struct {
// AllowExpr specifies a regular expression that matches the headers that can
// be mutated.
AllowExpr *regexp.Regexp
// DisallowExpr specifies a regular expression that matches the headers that
// cannot be mutated. This overrides the above allowExpr if a header matches
// both.
DisallowExpr *regexp.Regexp
// DisallowAll specifies that no header mutations are allowed. This overrides
// all other settings.
DisallowAll bool
// DisallowIsError specifies whether to return an error if a header mutation
// is disallowed. If true, the data plane RPC will be failed with a grpc
// status code of Unknown.
DisallowIsError bool
}

// ConvertStringMatchers converts a slice of protobuf StringMatcher messages to
// a slice of matcher.StringMatcher.
func ConvertStringMatchers(patterns []*v3matcherpb.StringMatcher) ([]matcher.StringMatcher, error) {
matchers := make([]matcher.StringMatcher, 0, len(patterns))
for _, p := range patterns {
sm, err := matcher.StringMatcherFromProto(p)
if err != nil {
return nil, err
}
matchers = append(matchers, sm)
}
return matchers, nil
}

// HeaderMutationRulesFromProto converts a protobuf HeaderMutationRules proto
// message to a HeaderMutationRules struct.
func HeaderMutationRulesFromProto(mr *v3mutationpb.HeaderMutationRules) (HeaderMutationRules, error) {
var rules HeaderMutationRules
if mr == nil {
return rules, nil
}
if allowExpr := mr.GetAllowExpression(); allowExpr != nil {
re, err := regexp.Compile(allowExpr.GetRegex())
if err != nil {
return rules, fmt.Errorf("httpfilter: %v", err)
}
rules.AllowExpr = re
}
if disallowExpr := mr.GetDisallowExpression(); disallowExpr != nil {
re, err := regexp.Compile(disallowExpr.GetRegex())
if err != nil {
return rules, fmt.Errorf("httpfilter: %v", err)
}
rules.DisallowExpr = re
}
rules.DisallowAll = mr.GetDisallowAll().GetValue()
rules.DisallowIsError = mr.GetDisallowIsError().GetValue()
Comment thread
eshitachandwani marked this conversation as resolved.
return rules, nil
}
102 changes: 102 additions & 0 deletions internal/xds/httpfilter/extconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
*
* Copyright 2026 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 httpfilter

import (
"regexp"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/protobuf/types/known/wrapperspb"

v3mutationpb "github.com/envoyproxy/go-control-plane/envoy/config/common/mutation_rules/v3"
v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
)

type s struct {
grpctest.Tester
}

func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}

func (s) TestHeaderMutationRulesFromProto_HappyPath(t *testing.T) {
mr := &v3mutationpb.HeaderMutationRules{
AllowExpression: &v3matcherpb.RegexMatcher{Regex: "^allow$"},
DisallowExpression: &v3matcherpb.RegexMatcher{Regex: "^disallow$"},
DisallowAll: wrapperspb.Bool(true),
DisallowIsError: wrapperspb.Bool(false),
}
want := HeaderMutationRules{
AllowExpr: regexp.MustCompile("^allow$"),
DisallowExpr: regexp.MustCompile("^disallow$"),
DisallowAll: true,
DisallowIsError: false,
}

got, err := HeaderMutationRulesFromProto(mr)
if err != nil {
t.Fatalf("HeaderMutationRulesFromProto() unexpected error: %v", err)
}
if diff := cmp.Diff(got, want,
cmp.Transformer("RegexpToString", func(r *regexp.Regexp) string {
if r == nil {
return ""
}
return r.String()
}),
); diff != "" {
t.Fatalf("HeaderMutationRulesFromProto() mismatch (-got +want):\n%s", diff)
}
}

func (s) TestHeaderMutationRulesFromProto_Errors(t *testing.T) {
tests := []struct {
name string
mutationRules *v3mutationpb.HeaderMutationRules
wantErr string
}{
{
name: "invalid allow expression",
mutationRules: &v3mutationpb.HeaderMutationRules{
AllowExpression: &v3matcherpb.RegexMatcher{Regex: "["},
},
wantErr: "httpfilter: error parsing regexp",
},
{
name: "invalid disallow expression",
mutationRules: &v3mutationpb.HeaderMutationRules{
DisallowExpression: &v3matcherpb.RegexMatcher{Regex: "["},
},
wantErr: "httpfilter: error parsing regexp",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := HeaderMutationRulesFromProto(tt.mutationRules)
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("HeaderMutationRulesFromProto() error = %v, wantErr %q", err, tt.wantErr)
}
})
}
}
Loading
Loading