From 792f1c7ed759b97ee6dc80246cf2de054c09a12f Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Wed, 2 Oct 2024 08:34:04 -0400 Subject: [PATCH] caddyhttp: Escaping placeholders in CEL, add `vars` and `vars_regexp` (#6594) * caddyhttp: Escaping placeholders in CEL * Simplify some of the test cases * Implement vars and vars_regexp in CEL * dupl lint is dumb * Better consts for the placeholder CEL shortcut * Bump CEL version, register a few extensions * Refactor s390x test script for readability * Add retries for s390x to smooth over flakiness * Switch to `ph` for the CEL shortcut (match it in templates cause why not) --- .github/workflows/ci.yml | 28 ++++- .golangci.yml | 6 + go.mod | 2 +- go.sum | 4 +- modules/caddyhttp/celmatcher.go | 51 ++++++--- modules/caddyhttp/celmatcher_test.go | 133 +++++++++++++++++----- modules/caddyhttp/fileserver/matcher.go | 10 +- modules/caddyhttp/matchers.go | 4 +- modules/caddyhttp/templates/templates.go | 6 + modules/caddyhttp/templates/tplcontext.go | 1 + modules/caddyhttp/vars.go | 89 +++++++++++++++ 11 files changed, 276 insertions(+), 58 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c7a67c5..3a74d8cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,13 +156,35 @@ jobs: # short sha is enough? short_sha=$(git rev-parse --short HEAD) + # To shorten the following lines + ssh_opts="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + ssh_host="$CI_USER@ci-s390x.caddyserver.com" + # The environment is fresh, so there's no point in keeping accepting and adding the key. - rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha" - ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -p 1 -tags nobadger -v ./..." + rsync -arz -e "ssh $ssh_opts" --progress --delete --exclude '.git' . "$ssh_host":/var/tmp/"$short_sha" + ssh $ssh_opts -t "$ssh_host" bash < 0)); do + CGO_ENABLED=0 go test -p 1 -tags nobadger -v ./... + exit_code=$? + if ((exit_code == 0)); then + break + fi + echo "\n\nTest failed: \$exit_code, retrying..." + ((retries--)) + done + echo "Remote exit code: \$exit_code" + exit \$exit_code + EOF test_result=$? # There's no need leaving the files around - ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$CI_USER"@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'" + ssh $ssh_opts "$ssh_host" "rm -rf /var/tmp/'$short_sha'" echo "Test exit code: $test_result" exit $test_result diff --git a/.golangci.yml b/.golangci.yml index e8b2a55d..aecff563 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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 diff --git a/go.mod b/go.mod index 0c4f1edd..62fe30de 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/caddyserver/zerossl v0.1.3 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.0.12 - github.com/google/cel-go v0.20.1 + github.com/google/cel-go v0.21.0 github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.17.8 github.com/klauspost/cpuid/v2 v2.2.7 diff --git a/go.sum b/go.sum index 6bf0bd3e..1f741c10 100644 --- a/go.sum +++ b/go.sum @@ -198,8 +198,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= -github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= +github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI= +github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go index a5565eb9..2a03ebba 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 25fdcf45..26491b7c 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({'Field': '\\\{foobar}'})`, + }, + urlTarget: "https://example.com/foo", + 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')`, }, - urlTarget: "https://example.com/foo", - httpHeader: &http.Header{"Field": []string{"foo", "bar"}}, - wantErr: true, + 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 6ab2180a..71de1db2 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 a0bc6d63..93a36237 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 0eba4870..eb648865 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 2324586b..1b1020f1 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 9e86dd71..77e06e3c 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 {