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)
This commit is contained in:
Francis Lavoie 2024-10-02 08:34:04 -04:00 committed by GitHub
parent c8adb1b553
commit 792f1c7ed7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 276 additions and 58 deletions

View File

@ -156,13 +156,35 @@ jobs:
# short sha is enough? # short sha is enough?
short_sha=$(git rev-parse --short HEAD) 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. # 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" rsync -arz -e "ssh $ssh_opts" --progress --delete --exclude '.git' . "$ssh_host":/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 ./..." 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=$? test_result=$?
# There's no need leaving the files around # 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" echo "Test exit code: $test_result"
exit $test_result exit $test_result

View File

@ -171,6 +171,12 @@ issues:
- path: modules/logging/filters.go - path: modules/logging/filters.go
linters: linters:
- dupl - dupl
- path: modules/caddyhttp/matchers.go
linters:
- dupl
- path: modules/caddyhttp/vars.go
linters:
- dupl
- path: _test\.go - path: _test\.go
linters: linters:
- errcheck - errcheck

2
go.mod
View File

@ -13,7 +13,7 @@ require (
github.com/caddyserver/zerossl v0.1.3 github.com/caddyserver/zerossl v0.1.3
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/go-chi/chi/v5 v5.0.12 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/google/uuid v1.6.0
github.com/klauspost/compress v1.17.8 github.com/klauspost/compress v1.17.8
github.com/klauspost/cpuid/v2 v2.2.7 github.com/klauspost/cpuid/v2 v2.2.7

4
go.sum
View File

@ -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 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 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 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.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= 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.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 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=

View File

@ -126,6 +126,10 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
// light (and possibly naïve) syntactic sugar // light (and possibly naïve) syntactic sugar
m.expandedExpr = placeholderRegexp.ReplaceAllString(m.Expr, placeholderExpansion) 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 // our type adapter expands CEL's standard type support
m.ta = celTypeAdapter{} m.ta = celTypeAdapter{}
@ -159,14 +163,17 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
// create the CEL environment // create the CEL environment
env, err := cel.NewEnv( env, err := cel.NewEnv(
cel.Function(placeholderFuncName, cel.SingletonBinaryBinding(m.caddyPlaceholderFunc), cel.Overload( cel.Function(CELPlaceholderFuncName, cel.SingletonBinaryBinding(m.caddyPlaceholderFunc), cel.Overload(
placeholderFuncName+"_httpRequest_string", CELPlaceholderFuncName+"_httpRequest_string",
[]*cel.Type{httpRequestObjectType, cel.StringType}, []*cel.Type{httpRequestObjectType, cel.StringType},
cel.AnyType, cel.AnyType,
)), )),
cel.Variable("request", httpRequestObjectType), cel.Variable(CELRequestVarName, httpRequestObjectType),
cel.CustomTypeAdapter(m.ta), cel.CustomTypeAdapter(m.ta),
ext.Strings(), ext.Strings(),
ext.Bindings(),
ext.Lists(),
ext.Math(),
matcherLib, matcherLib,
) )
if err != nil { if err != nil {
@ -247,7 +254,7 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
return types.NewErr( return types.NewErr(
"invalid request of type '%v' to %s(request, placeholderVarName)", "invalid request of type '%v' to %s(request, placeholderVarName)",
lhs.Type(), lhs.Type(),
placeholderFuncName, CELPlaceholderFuncName,
) )
} }
phStr, ok := rhs.(types.String) phStr, ok := rhs.(types.String)
@ -255,7 +262,7 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
return types.NewErr( return types.NewErr(
"invalid placeholder variable name of type '%v' to %s(request, placeholderVarName)", "invalid placeholder variable name of type '%v' to %s(request, placeholderVarName)",
rhs.Type(), rhs.Type(),
placeholderFuncName, CELPlaceholderFuncName,
) )
} }
@ -275,7 +282,7 @@ var httpRequestCELType = cel.ObjectType("http.Request", traits.ReceiverType)
type celHTTPRequest struct{ *http.Request } type celHTTPRequest struct{ *http.Request }
func (cr celHTTPRequest) ResolveName(name string) (any, bool) { func (cr celHTTPRequest) ResolveName(name string) (any, bool) {
if name == "request" { if name == CELRequestVarName {
return cr, true return cr, true
} }
return nil, false return nil, false
@ -457,15 +464,15 @@ func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.Int
callArgs := call.Args() callArgs := call.Args()
reqAttr, ok := callArgs[0].(interpreter.InterpretableAttribute) reqAttr, ok := callArgs[0].(interpreter.InterpretableAttribute)
if !ok { if !ok {
return nil, errors.New("missing 'request' argument") return nil, errors.New("missing 'req' argument")
} }
nsAttr, ok := reqAttr.Attr().(interpreter.NamespacedAttribute) nsAttr, ok := reqAttr.Attr().(interpreter.NamespacedAttribute)
if !ok { if !ok {
return nil, errors.New("missing 'request' argument") return nil, errors.New("missing 'req' argument")
} }
varNames := nsAttr.CandidateVariableNames() varNames := nsAttr.CandidateVariableNames()
if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != "request" { if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != CELRequestVarName {
return nil, errors.New("missing 'request' argument") return nil, errors.New("missing 'req' argument")
} }
matcherData, ok := callArgs[1].(interpreter.InterpretableConst) matcherData, ok := callArgs[1].(interpreter.InterpretableConst)
if !ok { 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 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") return nil, eh.NewError(0, "matcher requires one argument")
} }
if isCELStringExpr(args[0]) { 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") 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 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: case ast.UnspecifiedExprKind, ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.SelectKind:
// appeasing the linter :) // appeasing the linter :)
} }
@ -646,7 +653,7 @@ func isCELCaddyPlaceholderCall(e ast.Expr) bool {
switch e.Kind() { switch e.Kind() {
case ast.CallKind: case ast.CallKind:
call := e.AsCall() call := e.AsCall()
if call.FunctionName() == "caddyPlaceholder" { if call.FunctionName() == CELPlaceholderFuncName {
return true return true
} }
case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind: 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 // expressions with a proper CEL function call; this is
// just for syntactic sugar. // just for syntactic sugar.
var ( var (
placeholderRegexp = regexp.MustCompile(`{([a-zA-Z][\w.-]+)}`) // The placeholder may not be preceded by a backslash; the expansion
placeholderExpansion = `caddyPlaceholder(request, "${1}")` // 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) CELTypeJSON = cel.MapType(cel.StringType, cel.DynType)
) )
@ -710,7 +724,10 @@ var (
var httpRequestObjectType = cel.ObjectType("http.Request") var httpRequestObjectType = cel.ObjectType("http.Request")
// The name of the CEL function which accesses Replacer values. // 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" const MatcherNameCtxKey = "matcher_name"

View File

@ -70,12 +70,35 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
wantResult: true, 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{ expression: &MatchExpression{
Expr: `header('foo')`, Expr: `header('foo')`,
}, },
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantErr: true, wantErr: true,
}, },
{ {
@ -110,8 +133,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{ expression: &MatchExpression{
Expr: `header_regexp('foo')`, Expr: `header_regexp('foo')`,
}, },
urlTarget: "https://example.com/foo",
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
wantErr: true, wantErr: true,
}, },
{ {
@ -143,7 +164,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{ expression: &MatchExpression{
Expr: `host(80)`, Expr: `host(80)`,
}, },
urlTarget: "http://localhost:80",
wantErr: true, wantErr: true,
}, },
{ {
@ -169,8 +189,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{ expression: &MatchExpression{
Expr: `method()`, Expr: `method()`,
}, },
urlTarget: "https://foo.example.com",
httpMethod: "PUT",
wantErr: true, wantErr: true,
}, },
{ {
@ -266,7 +284,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{ expression: &MatchExpression{
Expr: `protocol()`, Expr: `protocol()`,
}, },
urlTarget: "https://example.com",
wantErr: true, wantErr: true,
}, },
{ {
@ -274,7 +291,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{ expression: &MatchExpression{
Expr: `protocol('grpc', 'https')`, Expr: `protocol('grpc', 'https')`,
}, },
urlTarget: "https://example.com",
wantErr: true, wantErr: true,
}, },
{ {
@ -282,7 +298,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{ expression: &MatchExpression{
Expr: `protocol(true)`, Expr: `protocol(true)`,
}, },
urlTarget: "https://example.com",
wantErr: true, wantErr: true,
}, },
{ {
@ -330,7 +345,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{ expression: &MatchExpression{
Expr: `query({1: "1"})`, Expr: `query({1: "1"})`,
}, },
urlTarget: "https://example.com/foo",
wantErr: true, wantErr: true,
}, },
{ {
@ -338,7 +352,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{ expression: &MatchExpression{
Expr: `query(Message{field: "1"})`, Expr: `query(Message{field: "1"})`,
}, },
urlTarget: "https://example.com/foo",
wantErr: true, wantErr: true,
}, },
{ {
@ -346,7 +359,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{ expression: &MatchExpression{
Expr: `query({"debug": 1})`, Expr: `query({"debug": 1})`,
}, },
urlTarget: "https://example.com/foo/?debug=1",
wantErr: true, wantErr: true,
}, },
{ {
@ -354,7 +366,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{ expression: &MatchExpression{
Expr: `query()`, Expr: `query()`,
}, },
urlTarget: "https://example.com/foo/?debug=1",
wantErr: true, wantErr: true,
}, },
{ {
@ -362,7 +373,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{ expression: &MatchExpression{
Expr: `remote_ip()`, Expr: `remote_ip()`,
}, },
urlTarget: "https://example.com/foo",
wantErr: true, wantErr: true,
}, },
{ {
@ -373,6 +383,67 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
urlTarget: "https://example.com/foo", urlTarget: "https://example.com/foo",
wantResult: true, 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() repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
"foo": "bar",
})
req = req.WithContext(ctx) req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
@ -436,6 +510,9 @@ func BenchmarkMatchExpressionMatch(b *testing.B) {
} }
repl := caddy.NewReplacer() repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
"foo": "bar",
})
req = req.WithContext(ctx) req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
if tc.clientCertificate != nil { if tc.clientCertificate != nil {

View File

@ -225,7 +225,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
return func(eh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) { return func(eh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) {
if len(args) == 0 { if len(args) == 0 {
return eh.NewCall("file", return eh.NewCall("file",
eh.NewIdent("request"), eh.NewIdent(caddyhttp.CELRequestVarName),
eh.NewMap(), eh.NewMap(),
), nil ), nil
} }
@ -233,7 +233,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
arg := args[0] arg := args[0]
if isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg) { if isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg) {
return eh.NewCall("file", return eh.NewCall("file",
eh.NewIdent("request"), eh.NewIdent(caddyhttp.CELRequestVarName),
eh.NewMap(eh.NewMapEntry( eh.NewMap(eh.NewMapEntry(
eh.NewLiteral(types.String("try_files")), eh.NewLiteral(types.String("try_files")),
eh.NewList(arg), eh.NewList(arg),
@ -242,7 +242,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
), nil ), nil
} }
if isCELTryFilesLiteral(arg) { 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{ return nil, &common.Error{
Location: eh.OffsetLocation(arg.ID()), Location: eh.OffsetLocation(arg.ID()),
@ -259,7 +259,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
} }
} }
return eh.NewCall("file", return eh.NewCall("file",
eh.NewIdent("request"), eh.NewIdent(caddyhttp.CELRequestVarName),
eh.NewMap(eh.NewMapEntry( eh.NewMap(eh.NewMapEntry(
eh.NewLiteral(types.String("try_files")), eh.NewLiteral(types.String("try_files")),
eh.NewList(args...), eh.NewList(args...),
@ -634,7 +634,7 @@ func isCELCaddyPlaceholderCall(e ast.Expr) bool {
switch e.Kind() { switch e.Kind() {
case ast.CallKind: case ast.CallKind:
call := e.AsCall() call := e.AsCall()
if call.FunctionName() == "caddyPlaceholder" { if call.FunctionName() == caddyhttp.CELPlaceholderFuncName {
return true return true
} }
case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind: case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind:

View File

@ -1562,8 +1562,8 @@ var (
_ CELLibraryProducer = (*MatchHeader)(nil) _ CELLibraryProducer = (*MatchHeader)(nil)
_ CELLibraryProducer = (*MatchHeaderRE)(nil) _ CELLibraryProducer = (*MatchHeaderRE)(nil)
_ CELLibraryProducer = (*MatchProtocol)(nil) _ CELLibraryProducer = (*MatchProtocol)(nil)
// _ CELLibraryProducer = (*VarsMatcher)(nil) _ CELLibraryProducer = (*VarsMatcher)(nil)
// _ CELLibraryProducer = (*MatchVarsRE)(nil) _ CELLibraryProducer = (*MatchVarsRE)(nil)
_ json.Marshaler = (*MatchNot)(nil) _ json.Marshaler = (*MatchNot)(nil)
_ json.Unmarshaler = (*MatchNot)(nil) _ json.Unmarshaler = (*MatchNot)(nil)

View File

@ -81,6 +81,12 @@ func init() {
// {{placeholder "http.error.status_code"}} // {{placeholder "http.error.status_code"}}
// ``` // ```
// //
// As a shortcut, `ph` is an alias for `placeholder`.
//
// ```
// {{ph "http.request.method"}}
// ```
//
// ##### `.Host` // ##### `.Host`
// //
// Returns the hostname portion (no port) of the Host header of the HTTP request. // Returns the hostname portion (no port) of the Host header of the HTTP request.

View File

@ -88,6 +88,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
"fileStat": c.funcFileStat, "fileStat": c.funcFileStat,
"env": c.funcEnv, "env": c.funcEnv,
"placeholder": c.funcPlaceholder, "placeholder": c.funcPlaceholder,
"ph": c.funcPlaceholder, // shortcut
"fileExists": c.funcFileExists, "fileExists": c.funcFileExists,
"httpError": c.funcHTTPError, "httpError": c.funcHTTPError,
"humanize": c.funcHumanize, "humanize": c.funcHumanize,

View File

@ -18,8 +18,12 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"reflect"
"strings" "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"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
) )
@ -203,6 +207,28 @@ func (m VarsMatcher) Match(r *http.Request) bool {
return false 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. // 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}` // 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 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. // Validate validates m's regular expressions.
func (m MatchVarsRE) Validate() error { func (m MatchVarsRE) Validate() error {
for _, rm := range m { for _, rm := range m {