Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

caddyhttp: Escaping placeholders in CEL, add vars and vars_regexp #6594

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ issues:
- path: modules/logging/filters.go
linters:
- dupl
- path: modules/caddyhttp/matchers.go
linters:
- dupl
- path: modules/caddyhttp/vars.go
linters:
- dupl
- path: _test\.go
linters:
- errcheck
15 changes: 13 additions & 2 deletions modules/caddyhttp/celmatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
// light (and possibly naïve) syntactic sugar
m.expandedExpr = placeholderRegexp.ReplaceAllString(m.Expr, placeholderExpansion)

// as a second pass, we'll strip the escape character from an escaped
// placeholder, so that it can be used as an input to other CEL functions
m.expandedExpr = escapedPlaceholderRegexp.ReplaceAllString(m.expandedExpr, escapedPlaceholderExpansion)

// our type adapter expands CEL's standard type support
m.ta = celTypeAdapter{}

Expand Down Expand Up @@ -701,8 +705,15 @@ func isCELStringListLiteral(e ast.Expr) bool {
// expressions with a proper CEL function call; this is
// just for syntactic sugar.
var (
placeholderRegexp = regexp.MustCompile(`{([a-zA-Z][\w.-]+)}`)
placeholderExpansion = `caddyPlaceholder(request, "${1}")`
// The placeholder may not be preceded by a backslash; the expansion
// will include the preceding character if it is not a backslash.
placeholderRegexp = regexp.MustCompile(`([^\\]|^){([a-zA-Z][\w.-]+)}`)
placeholderExpansion = `${1}caddyPlaceholder(request, "${2}")`

// As a second pass, we need to strip the escape character in front of
// the placeholder, if it exists.
escapedPlaceholderRegexp = regexp.MustCompile(`\\{([a-zA-Z][\w.-]+)}`)
escapedPlaceholderExpansion = `{${1}}`

CELTypeJSON = cel.MapType(cel.StringType, cel.DynType)
)
Expand Down
133 changes: 105 additions & 28 deletions modules/caddyhttp/celmatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,36 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
wantResult: true,
},
{
name: "header error (MatchHeader)",
name: "header matches an escaped placeholder value (MatchHeader)",
expression: &MatchExpression{
Expr: `header('foo')`,
Expr: `header({'Field': '\\\{foobar}'})`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantErr: true,
httpHeader: &http.Header{"Field": []string{"{foobar}"}},
wantResult: true,
},
{
name: "header matches an placeholder replaced during the header matcher (MatchHeader)",
expression: &MatchExpression{
Expr: `header({'Field': '\{http.request.uri.path}'})`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"/foo"}},
wantResult: true,
},
{
name: "header error, invalid escape sequence (MatchHeader)",
expression: &MatchExpression{
Expr: `header({'Field': '\\{foobar}'})`,
},
wantErr: true,
},
{
name: "header error, needs to be JSON syntax with field as key (MatchHeader)",
expression: &MatchExpression{
Expr: `header('foo')`,
},
wantErr: true,
},
{
name: "header_regexp matches (MatchHeaderRE)",
Expand Down Expand Up @@ -110,9 +133,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `header_regexp('foo')`,
},
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantErr: true,
wantErr: true,
},
{
name: "host matches localhost (MatchHost)",
Expand Down Expand Up @@ -143,8 +164,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `host(80)`,
},
urlTarget: "http://localhost:80",
wantErr: true,
wantErr: true,
},
{
name: "method does not match (MatchMethod)",
Expand All @@ -169,9 +189,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `method()`,
},
urlTarget: "https://foo.example.com",
httpMethod: "PUT",
wantErr: true,
wantErr: true,
},
{
name: "path matches substring (MatchPath)",
Expand Down Expand Up @@ -266,24 +284,21 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `protocol()`,
},
urlTarget: "https://example.com",
wantErr: true,
wantErr: true,
},
{
name: "protocol invocation error too many args (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol('grpc', 'https')`,
},
urlTarget: "https://example.com",
wantErr: true,
wantErr: true,
},
{
name: "protocol invocation error wrong arg type (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol(true)`,
},
urlTarget: "https://example.com",
wantErr: true,
wantErr: true,
},
{
name: "query does not match against a specific value (MatchQuery)",
Expand Down Expand Up @@ -330,40 +345,35 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `query({1: "1"})`,
},
urlTarget: "https://example.com/foo",
wantErr: true,
wantErr: true,
},
{
name: "query error typed struct instead of map (MatchQuery)",
expression: &MatchExpression{
Expr: `query(Message{field: "1"})`,
},
urlTarget: "https://example.com/foo",
wantErr: true,
wantErr: true,
},
{
name: "query error bad map value type (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": 1})`,
},
urlTarget: "https://example.com/foo/?debug=1",
wantErr: true,
wantErr: true,
},
{
name: "query error no args (MatchQuery)",
expression: &MatchExpression{
Expr: `query()`,
},
urlTarget: "https://example.com/foo/?debug=1",
wantErr: true,
wantErr: true,
},
{
name: "remote_ip error no args (MatchRemoteIP)",
expression: &MatchExpression{
Expr: `remote_ip()`,
},
urlTarget: "https://example.com/foo",
wantErr: true,
wantErr: true,
},
{
name: "remote_ip single IP match (MatchRemoteIP)",
Expand All @@ -373,6 +383,67 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "vars value (VarsMatcher)",
expression: &MatchExpression{
Expr: `vars({'foo': 'bar'})`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "vars matches placeholder, needs escape (VarsMatcher)",
expression: &MatchExpression{
Expr: `vars({'\{http.request.uri.path}': '/foo'})`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "vars error wrong syntax (VarsMatcher)",
expression: &MatchExpression{
Expr: `vars('foo', 'bar')`,
},
wantErr: true,
},
{
name: "vars error no args (VarsMatcher)",
expression: &MatchExpression{
Expr: `vars()`,
},
wantErr: true,
},
{
name: "vars_regexp value (MatchVarsRE)",
expression: &MatchExpression{
Expr: `vars_regexp('foo', 'ba?r')`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "vars_regexp value with name (MatchVarsRE)",
expression: &MatchExpression{
Expr: `vars_regexp('name', 'foo', 'ba?r')`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "vars_regexp matches placeholder, needs escape (MatchVarsRE)",
expression: &MatchExpression{
Expr: `vars_regexp('\{http.request.uri.path}', '/fo?o')`,
},
urlTarget: "https://example.com/foo",
wantResult: true,
},
{
name: "vars_regexp error no args (MatchVarsRE)",
expression: &MatchExpression{
Expr: `vars_regexp()`,
},
wantErr: true,
},
}
)

Expand All @@ -396,6 +467,9 @@ func TestMatchExpressionMatch(t *testing.T) {
}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
"foo": "bar",
})
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())

Expand Down Expand Up @@ -436,6 +510,9 @@ func BenchmarkMatchExpressionMatch(b *testing.B) {
}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
"foo": "bar",
})
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
if tc.clientCertificate != nil {
Expand Down
4 changes: 2 additions & 2 deletions modules/caddyhttp/matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1562,8 +1562,8 @@ var (
_ CELLibraryProducer = (*MatchHeader)(nil)
_ CELLibraryProducer = (*MatchHeaderRE)(nil)
_ CELLibraryProducer = (*MatchProtocol)(nil)
// _ CELLibraryProducer = (*VarsMatcher)(nil)
// _ CELLibraryProducer = (*MatchVarsRE)(nil)
_ CELLibraryProducer = (*VarsMatcher)(nil)
_ CELLibraryProducer = (*MatchVarsRE)(nil)

_ json.Marshaler = (*MatchNot)(nil)
_ json.Unmarshaler = (*MatchNot)(nil)
Expand Down
89 changes: 89 additions & 0 deletions modules/caddyhttp/vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ import (
"context"
"fmt"
"net/http"
"reflect"
"strings"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
Expand Down Expand Up @@ -203,6 +207,28 @@ func (m VarsMatcher) Match(r *http.Request) bool {
return false
}

// CELLibrary produces options that expose this matcher for use in CEL
// expression matchers.
//
// Example:
//
// expression vars({'{magic_number}': ['3', '5']})
// expression vars({'{foo}': 'single_value'})
func (VarsMatcher) CELLibrary(_ caddy.Context) (cel.Library, error) {
return CELMatcherImpl(
"vars",
"vars_matcher_request_map",
[]*cel.Type{CELTypeJSON},
func(data ref.Val) (RequestMatcher, error) {
mapStrListStr, err := CELValueToMapStrList(data)
if err != nil {
return nil, err
}
return VarsMatcher(mapStrListStr), nil
},
)
}

// MatchVarsRE matches the value of the context variables by a given regular expression.
//
// Upon a match, it adds placeholders to the request: `{http.regexp.name.capture_group}`
Expand Down Expand Up @@ -302,6 +328,69 @@ func (m MatchVarsRE) Match(r *http.Request) bool {
return false
}

// CELLibrary produces options that expose this matcher for use in CEL
// expression matchers.
//
// Example:
//
// expression vars_regexp('foo', '{magic_number}', '[0-9]+')
// expression vars_regexp('{magic_number}', '[0-9]+')
func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
unnamedPattern, err := CELMatcherImpl(
"vars_regexp",
"vars_regexp_request_string_string",
[]*cel.Type{cel.StringType, cel.StringType},
func(data ref.Val) (RequestMatcher, error) {
refStringList := reflect.TypeOf([]string{})
params, err := data.ConvertToNative(refStringList)
if err != nil {
return nil, err
}
strParams := params.([]string)
matcher := MatchVarsRE{}
matcher[strParams[0]] = &MatchRegexp{
Pattern: strParams[1],
Name: ctx.Value(MatcherNameCtxKey).(string),
}
err = matcher.Provision(ctx)
return matcher, err
},
)
if err != nil {
return nil, err
}
namedPattern, err := CELMatcherImpl(
"vars_regexp",
"vars_regexp_request_string_string_string",
[]*cel.Type{cel.StringType, cel.StringType, cel.StringType},
func(data ref.Val) (RequestMatcher, error) {
refStringList := reflect.TypeOf([]string{})
params, err := data.ConvertToNative(refStringList)
if err != nil {
return nil, err
}
strParams := params.([]string)
name := strParams[0]
if name == "" {
name = ctx.Value(MatcherNameCtxKey).(string)
}
matcher := MatchVarsRE{}
matcher[strParams[1]] = &MatchRegexp{
Pattern: strParams[2],
Name: name,
}
err = matcher.Provision(ctx)
return matcher, err
},
)
if err != nil {
return nil, err
}
envOpts := append(unnamedPattern.CompileOptions(), namedPattern.CompileOptions()...)
prgOpts := append(unnamedPattern.ProgramOptions(), namedPattern.ProgramOptions()...)
return NewMatcherCELLibrary(envOpts, prgOpts), nil
}

// Validate validates m's regular expressions.
func (m MatchVarsRE) Validate() error {
for _, rm := range m {
Expand Down
Loading