Skip to content

Commit 37d32a6

Browse files
feat: Add regex matching for request headers (#17)
* add regex MD * refactor: Join header values for regex matching
1 parent d08cbf7 commit 37d32a6

File tree

5 files changed

+85
-48
lines changed

5 files changed

+85
-48
lines changed

pkg/discovery/discovery.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@ package discovery
44
import (
55
"context"
66
"regexp"
7-
"slices"
87
"strconv"
98
"strings"
109

10+
"google.golang.org/grpc"
1111
"google.golang.org/grpc/metadata"
1212
"google.golang.org/grpc/status"
1313
"google.golang.org/protobuf/proto"
1414
"google.golang.org/protobuf/protoadapt"
15-
"google.golang.org/grpc"
1615
)
1716

1817
// Provider provides routing rules for the Service.
@@ -95,7 +94,8 @@ type RequestMatcher struct {
9594
URI *regexp.Regexp
9695

9796
// IncomingMetadata contains the metadata of the incoming request.
98-
IncomingMetadata metadata.MD
97+
// The key is the metadata key, and the value is the regexp to match against the metadata value.
98+
IncomingMetadata map[string]*regexp.Regexp
9999

100100
// Message contains the expected first RECV message of the request.
101101
Message proto.Message
@@ -107,8 +107,14 @@ func (r RequestMatcher) Matches(uri string, md metadata.MD) bool {
107107
return false
108108
}
109109

110-
for k, v := range r.IncomingMetadata {
111-
if !slices.Equal(v, md.Get(k)) {
110+
for k, re := range r.IncomingMetadata {
111+
vals := md.Get(k)
112+
if len(vals) == 0 {
113+
return false
114+
}
115+
116+
// Join multiple values into a single string to allow matching against the whole.
117+
if !re.MatchString(strings.Join(vals, ",")) {
112118
return false
113119
}
114120
}

pkg/discovery/discovery_test.go

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ func TestRule_String(t *testing.T) {
1414
got := (&Rule{
1515
Name: "name",
1616
Match: RequestMatcher{
17-
IncomingMetadata: metadata.New(map[string]string{
18-
"key": "value",
19-
"key2": "value2",
20-
}),
17+
IncomingMetadata: map[string]*regexp.Regexp{
18+
"key": regexp.MustCompile("value"),
19+
"key2": regexp.MustCompile("value2"),
20+
},
2121
Message: &errdetails.RequestInfo{
2222
RequestId: "request-id",
2323
ServingData: "serving-data",
@@ -32,10 +32,30 @@ func TestRule_String(t *testing.T) {
3232
}
3333

3434
func TestRequestMatcher_Matches(t *testing.T) {
35-
rm := RequestMatcher{
36-
URI: regexp.MustCompile(`^/v1/example/.*$`),
37-
IncomingMetadata: metadata.New(map[string]string{"key": "value"}),
38-
}
39-
assert.True(t, rm.Matches("/v1/example/123", metadata.New(map[string]string{"key": "value"})))
40-
assert.False(t, rm.Matches("/v1/example/123", metadata.New(map[string]string{"key": "value2"})))
35+
t.Run("single value", func(t *testing.T) {
36+
rm := RequestMatcher{
37+
URI: regexp.MustCompile(`^/v1/example/.*$`),
38+
IncomingMetadata: map[string]*regexp.Regexp{"key": regexp.MustCompile("^value.*$")},
39+
}
40+
assert.True(t, rm.Matches("/v1/example/123", metadata.New(map[string]string{"key": "value"})))
41+
assert.True(t, rm.Matches("/v1/example/123", metadata.New(map[string]string{"key": "value123"})))
42+
assert.False(t, rm.Matches("/v1/example/123", metadata.New(map[string]string{"key": "v"})))
43+
assert.False(t, rm.Matches("/v1/example/123", metadata.New(map[string]string{"another": "value"})))
44+
assert.False(t, rm.Matches("/v2/example/123", metadata.New(map[string]string{"key": "value"})))
45+
})
46+
47+
t.Run("multiple values", func(t *testing.T) {
48+
rm := RequestMatcher{
49+
IncomingMetadata: map[string]*regexp.Regexp{"key": regexp.MustCompile("^v1,v2$")},
50+
}
51+
52+
md := metadata.Pairs("key", "v1", "key", "v2")
53+
assert.True(t, rm.Matches("any-uri", md))
54+
55+
md = metadata.Pairs("key", "v1")
56+
assert.False(t, rm.Matches("any-uri", md))
57+
58+
md = metadata.Pairs("key", "v2", "key", "v1")
59+
assert.False(t, rm.Matches("any-uri", md), "should not match on wrong order")
60+
})
4161
}

pkg/discovery/fileprovider/file.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,20 @@ import (
99
"regexp"
1010
"time"
1111

12+
"crypto/tls"
13+
"sort"
14+
1215
"github.com/Semior001/groxy/pkg/discovery"
16+
"github.com/Semior001/groxy/pkg/grpcx"
1317
"github.com/Semior001/groxy/pkg/protodef"
1418
"github.com/cappuccinotm/slogx"
19+
"google.golang.org/grpc"
1520
"google.golang.org/grpc/codes"
21+
"google.golang.org/grpc/credentials"
22+
"google.golang.org/grpc/credentials/insecure"
1623
"google.golang.org/grpc/metadata"
1724
"google.golang.org/grpc/status"
1825
"gopkg.in/yaml.v3"
19-
"google.golang.org/grpc"
20-
"google.golang.org/grpc/credentials/insecure"
21-
"google.golang.org/grpc/credentials"
22-
"crypto/tls"
23-
"sort"
24-
"github.com/Semior001/groxy/pkg/grpcx"
2526
)
2627

2728
// File discovers the changes in routing rules from a file.
@@ -219,7 +220,16 @@ func parseRule(r Rule, upstreams []discovery.Upstream) (result discovery.Rule, e
219220
return discovery.Rule{}, fmt.Errorf("compile URI regexp: %w", err)
220221
}
221222

222-
result.Match.IncomingMetadata = metadata.New(r.Match.Header)
223+
if len(r.Match.Header) > 0 {
224+
result.Match.IncomingMetadata = make(map[string]*regexp.Regexp, len(r.Match.Header))
225+
for k, v := range r.Match.Header {
226+
re, err := regexp.Compile(v)
227+
if err != nil {
228+
return discovery.Rule{}, fmt.Errorf("compile header %q regexp: %w", k, err)
229+
}
230+
result.Match.IncomingMetadata[k] = re
231+
}
232+
}
223233

224234
if r.Match.Body != nil {
225235
if result.Match.Message, err = protodef.BuildMessage(*r.Match.Body); err != nil {

pkg/discovery/fileprovider/file_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,31 +96,31 @@ func TestFile_Rules(t *testing.T) {
9696
Name: "com.github.Semior001.groxy.example.mock.ExampleService/Stub",
9797
Match: discovery.RequestMatcher{
9898
URI: regexp.MustCompile("com.github.Semior001.groxy.example.mock.ExampleService/Stub"),
99-
IncomingMetadata: map[string][]string{"test": {"true"}},
99+
IncomingMetadata: map[string]*regexp.Regexp{"test": regexp.MustCompile("true")},
100100
},
101101
Mock: &discovery.Mock{},
102102
},
103103
{
104104
Name: "com.github.Semior001.groxy.example.mock.ExampleService/Stub",
105105
Match: discovery.RequestMatcher{
106106
URI: regexp.MustCompile("com.github.Semior001.groxy.example.mock.ExampleService/Stub"),
107-
IncomingMetadata: metadata.New(nil),
107+
IncomingMetadata: nil,
108108
},
109109
Mock: &discovery.Mock{},
110110
},
111111
{
112112
Name: "com.github.Semior001.groxy.example.mock.ExampleService/Stub",
113113
Match: discovery.RequestMatcher{
114114
URI: regexp.MustCompile("com.github.Semior001.groxy.example.mock.ExampleService/Stub"),
115-
IncomingMetadata: metadata.New(nil),
115+
IncomingMetadata: nil,
116116
},
117117
Mock: &discovery.Mock{},
118118
},
119119
{
120120
Name: "com.github.Semior001.groxy.example.mock.ExampleService/Error",
121121
Match: discovery.RequestMatcher{
122122
URI: regexp.MustCompile("com.github.Semior001.groxy.example.mock.ExampleService/Error"),
123-
IncomingMetadata: metadata.New(nil),
123+
IncomingMetadata: nil,
124124
},
125125
Mock: &discovery.Mock{
126126
Status: status.New(codes.InvalidArgument, "invalid request"),
@@ -132,7 +132,7 @@ func TestFile_Rules(t *testing.T) {
132132
Name: "com.github.Semior001.groxy.example.mock.Upstream/Get",
133133
Match: discovery.RequestMatcher{
134134
URI: regexp.MustCompile("com.github.Semior001.groxy.example.mock.Upstream/Get"),
135-
IncomingMetadata: metadata.New(nil),
135+
IncomingMetadata: nil,
136136
},
137137
},
138138
{

pkg/discovery/service_test.go

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package discovery
22

33
import (
4+
"context"
5+
"errors"
46
"regexp"
57
"testing"
8+
"time"
9+
610
"github.com/stretchr/testify/assert"
711
"github.com/stretchr/testify/require"
812
"google.golang.org/genproto/googleapis/rpc/errdetails"
913
"google.golang.org/grpc/metadata"
1014
"google.golang.org/protobuf/proto"
11-
"context"
12-
"time"
13-
"errors"
1415
)
1516

1617
func TestMatches_NeedsDeeperMatch(t *testing.T) {
@@ -34,15 +35,15 @@ func TestService_MatchMetadata(t *testing.T) {
3435
rules: []*Rule{
3536
{Name: "1", Match: RequestMatcher{
3637
URI: regexp.MustCompile("test"),
37-
IncomingMetadata: metadata.New(map[string]string{"uri": "not-match"}),
38+
IncomingMetadata: map[string]*regexp.Regexp{"uri": regexp.MustCompile("not-match")},
3839
}},
3940
{Name: "2", Match: RequestMatcher{
4041
URI: regexp.MustCompile("uri"),
41-
IncomingMetadata: metadata.New(map[string]string{"uri": "test"}),
42+
IncomingMetadata: map[string]*regexp.Regexp{"uri": regexp.MustCompile("test")},
4243
}},
4344
{Name: "3", Match: RequestMatcher{
4445
URI: regexp.MustCompile("uri"),
45-
IncomingMetadata: metadata.New(map[string]string{"uri": "test"}),
46+
IncomingMetadata: map[string]*regexp.Regexp{"uri": regexp.MustCompile("test")},
4647
}},
4748
},
4849
}
@@ -95,7 +96,7 @@ func TestService_Run(t *testing.T) {
9596
StateFunc: func(context.Context) (*State, error) {
9697
return &State{Rules: []*Rule{
9798
{Name: "1", Match: RequestMatcher{}},
98-
{Name: "2", Match: RequestMatcher{IncomingMetadata: metadata.New(map[string]string{"uri": "test"})}},
99+
{Name: "2", Match: RequestMatcher{IncomingMetadata: map[string]*regexp.Regexp{"uri": regexp.MustCompile("test")}}},
99100
}}, nil
100101
},
101102
}
@@ -106,10 +107,10 @@ func TestService_Run(t *testing.T) {
106107
},
107108
StateFunc: func(context.Context) (*State, error) {
108109
return &State{Rules: []*Rule{
109-
{Name: "3", Match: RequestMatcher{IncomingMetadata: metadata.New(map[string]string{
110-
"uri": "test",
111-
"uri2": "test2",
112-
})}},
110+
{Name: "3", Match: RequestMatcher{IncomingMetadata: map[string]*regexp.Regexp{
111+
"uri": regexp.MustCompile("test"),
112+
"uri2": regexp.MustCompile("test2"),
113+
}}},
113114
}}, nil
114115
},
115116
}
@@ -129,11 +130,11 @@ func TestService_Run(t *testing.T) {
129130
require.Error(t, err)
130131
assert.Equal(t, context.DeadlineExceeded, err)
131132
assert.Equal(t, []*Rule{
132-
{Name: "3", Match: RequestMatcher{IncomingMetadata: metadata.New(map[string]string{
133-
"uri": "test",
134-
"uri2": "test2",
135-
})}},
136-
{Name: "2", Match: RequestMatcher{IncomingMetadata: metadata.New(map[string]string{"uri": "test"})}},
133+
{Name: "3", Match: RequestMatcher{IncomingMetadata: map[string]*regexp.Regexp{
134+
"uri": regexp.MustCompile("test"),
135+
"uri2": regexp.MustCompile("test2"),
136+
}}},
137+
{Name: "2", Match: RequestMatcher{IncomingMetadata: map[string]*regexp.Regexp{"uri": regexp.MustCompile("test")}}},
137138
{Name: "1", Match: RequestMatcher{}},
138139
}, svc.rules)
139140
})
@@ -151,7 +152,7 @@ func TestService_Run(t *testing.T) {
151152
StateFunc: func(context.Context) (*State, error) {
152153
return &State{Rules: []*Rule{
153154
{Name: "1", Match: RequestMatcher{}},
154-
{Name: "2", Match: RequestMatcher{IncomingMetadata: metadata.New(map[string]string{"uri": "test"})}},
155+
{Name: "2", Match: RequestMatcher{IncomingMetadata: map[string]*regexp.Regexp{"uri": regexp.MustCompile("test")}}},
155156
}}, nil
156157
},
157158
}
@@ -162,10 +163,10 @@ func TestService_Run(t *testing.T) {
162163
},
163164
StateFunc: func(context.Context) (*State, error) {
164165
return &State{Rules: []*Rule{
165-
{Name: "3", Match: RequestMatcher{IncomingMetadata: metadata.New(map[string]string{
166-
"uri": "test",
167-
"uri2": "test2",
168-
})}},
166+
{Name: "3", Match: RequestMatcher{IncomingMetadata: map[string]*regexp.Regexp{
167+
"uri": regexp.MustCompile("test"),
168+
"uri2": regexp.MustCompile("test2"),
169+
}}},
169170
}}, nil
170171
},
171172
}

0 commit comments

Comments
 (0)