diff options
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r-- | modules/caddyhttp/celmatcher.go | 51 | ||||
-rw-r--r-- | modules/caddyhttp/celmatcher_test.go | 133 | ||||
-rw-r--r-- | modules/caddyhttp/fileserver/matcher.go | 10 | ||||
-rw-r--r-- | modules/caddyhttp/matchers.go | 4 | ||||
-rw-r--r-- | modules/caddyhttp/templates/templates.go | 6 | ||||
-rw-r--r-- | modules/caddyhttp/templates/tplcontext.go | 1 | ||||
-rw-r--r-- | modules/caddyhttp/vars.go | 89 |
7 files changed, 242 insertions, 52 deletions
diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go index a5565eb98..2a03ebba7 100644 --- a/modules/caddyhttp/celmatcher.go +++ b/modules/caddyhttp/celmatcher.go @@ -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{} @@ -159,14 +163,17 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error { // create the CEL environment env, err := cel.NewEnv( - cel.Function(placeholderFuncName, cel.SingletonBinaryBinding(m.caddyPlaceholderFunc), cel.Overload( - placeholderFuncName+"_httpRequest_string", + cel.Function(CELPlaceholderFuncName, cel.SingletonBinaryBinding(m.caddyPlaceholderFunc), cel.Overload( + CELPlaceholderFuncName+"_httpRequest_string", []*cel.Type{httpRequestObjectType, cel.StringType}, cel.AnyType, )), - cel.Variable("request", httpRequestObjectType), + cel.Variable(CELRequestVarName, httpRequestObjectType), cel.CustomTypeAdapter(m.ta), ext.Strings(), + ext.Bindings(), + ext.Lists(), + ext.Math(), matcherLib, ) if err != nil { @@ -247,7 +254,7 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val { return types.NewErr( "invalid request of type '%v' to %s(request, placeholderVarName)", lhs.Type(), - placeholderFuncName, + CELPlaceholderFuncName, ) } phStr, ok := rhs.(types.String) @@ -255,7 +262,7 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val { return types.NewErr( "invalid placeholder variable name of type '%v' to %s(request, placeholderVarName)", rhs.Type(), - placeholderFuncName, + CELPlaceholderFuncName, ) } @@ -275,7 +282,7 @@ var httpRequestCELType = cel.ObjectType("http.Request", traits.ReceiverType) type celHTTPRequest struct{ *http.Request } func (cr celHTTPRequest) ResolveName(name string) (any, bool) { - if name == "request" { + if name == CELRequestVarName { return cr, true } return nil, false @@ -457,15 +464,15 @@ func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.Int callArgs := call.Args() reqAttr, ok := callArgs[0].(interpreter.InterpretableAttribute) if !ok { - return nil, errors.New("missing 'request' argument") + return nil, errors.New("missing 'req' argument") } nsAttr, ok := reqAttr.Attr().(interpreter.NamespacedAttribute) if !ok { - return nil, errors.New("missing 'request' argument") + return nil, errors.New("missing 'req' argument") } varNames := nsAttr.CandidateVariableNames() - if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != "request" { - return nil, errors.New("missing 'request' argument") + if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != CELRequestVarName { + return nil, errors.New("missing 'req' argument") } matcherData, ok := callArgs[1].(interpreter.InterpretableConst) if !ok { @@ -524,7 +531,7 @@ func celMatcherStringListMacroExpander(funcName string) cel.MacroFactory { return nil, eh.NewError(arg.ID(), "matcher arguments must be string constants") } } - return eh.NewCall(funcName, eh.NewIdent("request"), eh.NewList(matchArgs...)), nil + return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), eh.NewList(matchArgs...)), nil } } @@ -538,7 +545,7 @@ func celMatcherStringMacroExpander(funcName string) parser.MacroExpander { return nil, eh.NewError(0, "matcher requires one argument") } if isCELStringExpr(args[0]) { - return eh.NewCall(funcName, eh.NewIdent("request"), args[0]), nil + return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), args[0]), nil } return nil, eh.NewError(args[0].ID(), "matcher argument must be a string literal") } @@ -572,7 +579,7 @@ func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander { return nil, eh.NewError(entry.AsMapEntry().Value().ID(), "matcher map values must be string or list literals") } } - return eh.NewCall(funcName, eh.NewIdent("request"), arg), nil + return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), arg), nil case ast.UnspecifiedExprKind, ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.SelectKind: // appeasing the linter :) } @@ -646,7 +653,7 @@ func isCELCaddyPlaceholderCall(e ast.Expr) bool { switch e.Kind() { case ast.CallKind: call := e.AsCall() - if call.FunctionName() == "caddyPlaceholder" { + if call.FunctionName() == CELPlaceholderFuncName { return true } case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind: @@ -701,8 +708,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}ph(req, "${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) ) @@ -710,7 +724,10 @@ var ( var httpRequestObjectType = cel.ObjectType("http.Request") // The name of the CEL function which accesses Replacer values. -const placeholderFuncName = "caddyPlaceholder" +const CELPlaceholderFuncName = "ph" + +// The name of the CEL request variable. +const CELRequestVarName = "req" const MatcherNameCtxKey = "matcher_name" diff --git a/modules/caddyhttp/celmatcher_test.go b/modules/caddyhttp/celmatcher_test.go index 25fdcf45e..26491b7ca 100644 --- a/modules/caddyhttp/celmatcher_test.go +++ b/modules/caddyhttp/celmatcher_test.go @@ -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)", @@ -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)", @@ -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)", @@ -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)", @@ -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)", @@ -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)", @@ -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, + }, } ) @@ -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()) @@ -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 { diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index 6ab2180ad..71de1db29 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -225,7 +225,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander { return func(eh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { if len(args) == 0 { return eh.NewCall("file", - eh.NewIdent("request"), + eh.NewIdent(caddyhttp.CELRequestVarName), eh.NewMap(), ), nil } @@ -233,7 +233,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander { arg := args[0] if isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg) { return eh.NewCall("file", - eh.NewIdent("request"), + eh.NewIdent(caddyhttp.CELRequestVarName), eh.NewMap(eh.NewMapEntry( eh.NewLiteral(types.String("try_files")), eh.NewList(arg), @@ -242,7 +242,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander { ), nil } if isCELTryFilesLiteral(arg) { - return eh.NewCall("file", eh.NewIdent("request"), arg), nil + return eh.NewCall("file", eh.NewIdent(caddyhttp.CELRequestVarName), arg), nil } return nil, &common.Error{ Location: eh.OffsetLocation(arg.ID()), @@ -259,7 +259,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander { } } return eh.NewCall("file", - eh.NewIdent("request"), + eh.NewIdent(caddyhttp.CELRequestVarName), eh.NewMap(eh.NewMapEntry( eh.NewLiteral(types.String("try_files")), eh.NewList(args...), @@ -634,7 +634,7 @@ func isCELCaddyPlaceholderCall(e ast.Expr) bool { switch e.Kind() { case ast.CallKind: call := e.AsCall() - if call.FunctionName() == "caddyPlaceholder" { + if call.FunctionName() == caddyhttp.CELPlaceholderFuncName { return true } case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind: diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index a0bc6d63a..93a36237b 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -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) diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index 0eba4870f..eb6488659 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -81,6 +81,12 @@ func init() { // {{placeholder "http.error.status_code"}} // ``` // +// As a shortcut, `ph` is an alias for `placeholder`. +// +// ``` +// {{ph "http.request.method"}} +// ``` +// // ##### `.Host` // // Returns the hostname portion (no port) of the Host header of the HTTP request. diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go index 2324586be..1b1020f1b 100644 --- a/modules/caddyhttp/templates/tplcontext.go +++ b/modules/caddyhttp/templates/tplcontext.go @@ -88,6 +88,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template { "fileStat": c.funcFileStat, "env": c.funcEnv, "placeholder": c.funcPlaceholder, + "ph": c.funcPlaceholder, // shortcut "fileExists": c.funcFileExists, "httpError": c.funcHTTPError, "humanize": c.funcHumanize, diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go index 9e86dd716..77e06e3cb 100644 --- a/modules/caddyhttp/vars.go +++ b/modules/caddyhttp/vars.go @@ -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" ) @@ -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}` @@ -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 { |