From 10f85558ead15e119f8e9abd81c8ad55eb865f8b Mon Sep 17 00:00:00 2001 From: Tristan Swadell Date: Wed, 22 Jun 2022 15:53:46 -0700 Subject: Expose several Caddy HTTP Matchers to the CEL Matcher (#4715) Co-authored-by: Francis Lavoie --- modules/caddyhttp/matchers.go | 290 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 289 insertions(+), 1 deletion(-) (limited to 'modules/caddyhttp/matchers.go') diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index f8953ef..268b936 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -16,6 +16,7 @@ package caddyhttp import ( "encoding/json" + "errors" "fmt" "net" "net/http" @@ -23,6 +24,7 @@ import ( "net/url" "path" "path/filepath" + "reflect" "regexp" "sort" "strconv" @@ -30,7 +32,12 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker/decls" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" "go.uber.org/zap" + exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) type ( @@ -291,6 +298,29 @@ outer: return false } +// CELLibrary produces options that expose this matcher for use in CEL +// expression matchers. +// +// Example: +// expression host('localhost') +func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) { + return CELMatcherImpl( + "host", + "host_match_request_list", + []*exprpb.Type{CelTypeListString}, + func(data ref.Val) (RequestMatcher, error) { + refStringList := reflect.TypeOf([]string{}) + strList, err := data.ConvertToNative(refStringList) + if err != nil { + return nil, err + } + matcher := MatchHost(strList.([]string)) + err = matcher.Provision(ctx) + return matcher, err + }, + ) +} + // fuzzy returns true if the given hostname h is not a specific // hostname, e.g. has placeholders or wildcards. func (MatchHost) fuzzy(h string) bool { return strings.ContainsAny(h, "{*") } @@ -396,6 +426,33 @@ func (m MatchPath) Match(r *http.Request) bool { return false } +// CELLibrary produces options that expose this matcher for use in CEL +// expression matchers. +// +// Example: +// expression path('*substring*', '*suffix') +func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) { + return CELMatcherImpl( + // name of the macro, this is the function name that users see when writing expressions. + "path", + // name of the function that the macro will be rewritten to call. + "path_match_request_list", + // internal data type of the MatchPath value. + []*exprpb.Type{CelTypeListString}, + // function to convert a constant list of strings to a MatchPath instance. + func(data ref.Val) (RequestMatcher, error) { + refStringList := reflect.TypeOf([]string{}) + strList, err := data.ConvertToNative(refStringList) + if err != nil { + return nil, err + } + matcher := MatchPath(strList.([]string)) + err = matcher.Provision(ctx) + return matcher, err + }, + ) +} + // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { @@ -440,6 +497,50 @@ func (m MatchPathRE) Match(r *http.Request) bool { return m.MatchRegexp.Match(cleanedPath, repl) } +// CELLibrary produces options that expose this matcher for use in CEL +// expression matchers. +// +// Example: +// expression path_regexp('^/bar') +func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { + unnamedPattern, err := CELMatcherImpl( + "path_regexp", + "path_regexp_request_string", + []*exprpb.Type{decls.String}, + func(data ref.Val) (RequestMatcher, error) { + pattern := data.(types.String) + matcher := MatchPathRE{MatchRegexp{Pattern: string(pattern)}} + err := matcher.Provision(ctx) + return matcher, err + }, + ) + if err != nil { + return nil, err + } + namedPattern, err := CELMatcherImpl( + "path_regexp", + "path_regexp_request_string_string", + []*exprpb.Type{decls.String, decls.String}, + 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 := MatchPathRE{MatchRegexp{Name: strParams[0], Pattern: strParams[1]}} + 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 +} + // CaddyModule returns the Caddy module information. func (MatchMethod) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ @@ -469,6 +570,27 @@ func (m MatchMethod) Match(r *http.Request) bool { return false } +// CELLibrary produces options that expose this matcher for use in CEL +// expression matchers. +// +// Example: +// expression method('PUT', 'POST') +func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) { + return CELMatcherImpl( + "method", + "method_request_list", + []*exprpb.Type{CelTypeListString}, + func(data ref.Val) (RequestMatcher, error) { + refStringList := reflect.TypeOf([]string{}) + strList, err := data.ConvertToNative(refStringList) + if err != nil { + return nil, err + } + return MatchMethod(strList.([]string)), nil + }, + ) +} + // CaddyModule returns the Caddy module information. func (MatchQuery) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ @@ -518,6 +640,26 @@ func (m MatchQuery) Match(r *http.Request) bool { return len(m) == 0 && len(r.URL.Query()) == 0 } +// CELLibrary produces options that expose this matcher for use in CEL +// expression matchers. +// +// Example: +// expression query({'sort': 'asc'}) || query({'foo': ['*bar*', 'baz']}) +func (MatchQuery) CELLibrary(_ caddy.Context) (cel.Library, error) { + return CELMatcherImpl( + "query", + "query_matcher_request_map", + []*exprpb.Type{CelTypeJson}, + func(data ref.Val) (RequestMatcher, error) { + mapStrListStr, err := CELValueToMapStrList(data) + if err != nil { + return nil, err + } + return MatchQuery(url.Values(mapStrListStr)), nil + }, + ) +} + // CaddyModule returns the Caddy module information. func (MatchHeader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ @@ -573,6 +715,27 @@ func (m MatchHeader) Match(r *http.Request) bool { return matchHeaders(r.Header, http.Header(m), r.Host, repl) } +// CELLibrary produces options that expose this matcher for use in CEL +// expression matchers. +// +// Example: +// expression header({'content-type': 'image/png'}) +// expression header({'foo': ['bar', 'baz']}) // match bar or baz +func (MatchHeader) CELLibrary(_ caddy.Context) (cel.Library, error) { + return CELMatcherImpl( + "header", + "header_matcher_request_map", + []*exprpb.Type{CelTypeJson}, + func(data ref.Val) (RequestMatcher, error) { + mapStrListStr, err := CELValueToMapStrList(data) + if err != nil { + return nil, err + } + return MatchHeader(http.Header(mapStrListStr)), nil + }, + ) +} + // getHeaderFieldVals returns the field values for the given fieldName from input. // The host parameter should be obtained from the http.Request.Host field since // net/http removes it from the header map. @@ -710,6 +873,57 @@ func (m MatchHeaderRE) Validate() error { return nil } +// CELLibrary produces options that expose this matcher for use in CEL +// expression matchers. +// +// Example: +// expression header_regexp('foo', 'Field', 'fo+') +func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) { + unnamedPattern, err := CELMatcherImpl( + "header_regexp", + "header_regexp_request_string_string", + []*exprpb.Type{decls.String, decls.String}, + 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 := MatchHeaderRE{} + matcher[strParams[0]] = &MatchRegexp{Pattern: strParams[1], Name: ""} + err = matcher.Provision(ctx) + return matcher, err + }, + ) + if err != nil { + return nil, err + } + namedPattern, err := CELMatcherImpl( + "header_regexp", + "header_regexp_request_string_string_string", + []*exprpb.Type{decls.String, decls.String, decls.String}, + 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 := MatchHeaderRE{} + matcher[strParams[1]] = &MatchRegexp{Pattern: strParams[2], Name: strParams[0]} + 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 +} + // CaddyModule returns the Caddy module information. func (MatchProtocol) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ @@ -743,6 +957,26 @@ func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } +// CELLibrary produces options that expose this matcher for use in CEL +// expression matchers. +// +// Example: +// expression protocol('https') +func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) { + return CELMatcherImpl( + "protocol", + "protocol_request_string", + []*exprpb.Type{decls.String}, + func(data ref.Val) (RequestMatcher, error) { + protocolStr, ok := data.(types.String) + if !ok { + return nil, errors.New("protocol argument was not a string") + } + return MatchProtocol(strings.ToLower(string(protocolStr))), nil + }, + ) +} + // CaddyModule returns the Caddy module information. func (MatchNot) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ @@ -887,6 +1121,46 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } +// CELLibrary produces options that expose this matcher for use in CEL +// expression matchers. +// +// Example: +// expression remote_ip('forwarded', '192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8') +func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) { + return CELMatcherImpl( + // name of the macro, this is the function name that users see when writing expressions. + "remote_ip", + // name of the function that the macro will be rewritten to call. + "remote_ip_match_request_list", + // internal data type of the MatchPath value. + []*exprpb.Type{CelTypeListString}, + // function to convert a constant list of strings to a MatchPath instance. + func(data ref.Val) (RequestMatcher, error) { + refStringList := reflect.TypeOf([]string{}) + strList, err := data.ConvertToNative(refStringList) + if err != nil { + return nil, err + } + + m := MatchRemoteIP{} + + for _, input := range strList.([]string) { + if input == "forwarded" { + if len(m.Ranges) > 0 { + return nil, errors.New("if used, 'forwarded' must be first argument") + } + m.Forwarded = true + continue + } + m.Ranges = append(m.Ranges, input) + } + + err = m.Provision(ctx) + return m, err + }, + ) +} + // Provision parses m's IP ranges, either from IP or CIDR expressions. func (m *MatchRemoteIP) Provision(ctx caddy.Context) error { m.logger = ctx.Logger(m) @@ -1062,7 +1336,9 @@ func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } -var wordRE = regexp.MustCompile(`\w+`) +var ( + wordRE = regexp.MustCompile(`\w+`) +) const regexpPlaceholderPrefix = "http.regexp" @@ -1103,6 +1379,18 @@ var ( _ caddyfile.Unmarshaler = (*VarsMatcher)(nil) _ caddyfile.Unmarshaler = (*MatchVarsRE)(nil) + _ CELLibraryProducer = (*MatchHost)(nil) + _ CELLibraryProducer = (*MatchPath)(nil) + _ CELLibraryProducer = (*MatchPathRE)(nil) + _ CELLibraryProducer = (*MatchMethod)(nil) + _ CELLibraryProducer = (*MatchQuery)(nil) + _ CELLibraryProducer = (*MatchHeader)(nil) + _ CELLibraryProducer = (*MatchHeaderRE)(nil) + _ CELLibraryProducer = (*MatchProtocol)(nil) + _ CELLibraryProducer = (*MatchRemoteIP)(nil) + // _ CELLibraryProducer = (*VarsMatcher)(nil) + // _ CELLibraryProducer = (*MatchVarsRE)(nil) + _ json.Marshaler = (*MatchNot)(nil) _ json.Unmarshaler = (*MatchNot)(nil) ) -- cgit v1.2.3