aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFrancis Lavoie <[email protected]>2024-10-02 08:34:04 -0400
committerGitHub <[email protected]>2024-10-02 06:34:04 -0600
commit792f1c7ed759b97ee6dc80246cf2de054c09a12f (patch)
treebe3533923d2ba7dfb5a3fde75d440e1aa7463b88
parentc8adb1b553412253d5f166065635ab809d3eef33 (diff)
downloadcaddy-792f1c7ed759b97ee6dc80246cf2de054c09a12f.tar.gz
caddy-792f1c7ed759b97ee6dc80246cf2de054c09a12f.zip
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)
-rw-r--r--.github/workflows/ci.yml28
-rw-r--r--.golangci.yml6
-rw-r--r--go.mod2
-rw-r--r--go.sum4
-rw-r--r--modules/caddyhttp/celmatcher.go51
-rw-r--r--modules/caddyhttp/celmatcher_test.go133
-rw-r--r--modules/caddyhttp/fileserver/matcher.go10
-rw-r--r--modules/caddyhttp/matchers.go4
-rw-r--r--modules/caddyhttp/templates/templates.go6
-rw-r--r--modules/caddyhttp/templates/tplcontext.go1
-rw-r--r--modules/caddyhttp/vars.go89
11 files changed, 276 insertions, 58 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2c7a67c5b..3a74d8cc6 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="[email protected]"
+
# 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 <<EOF
+ cd /var/tmp/$short_sha
+ go version
+ go env
+ printf "\n\n"
+ retries=3
+ exit_code=0
+ while ((retries > 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 e8b2a55d3..aecff563e 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 0c4f1edd7..62fe30dee 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 6bf0bd3e2..1f741c104 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 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 {