mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
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:
parent
c8adb1b553
commit
792f1c7ed7
11 changed files with 276 additions and 58 deletions
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -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
2
go.mod
|
@ -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
4
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 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=
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -70,13 +70,36 @@ 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",
|
wantErr: true,
|
||||||
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "header_regexp matches (MatchHeaderRE)",
|
name: "header_regexp matches (MatchHeaderRE)",
|
||||||
|
@ -110,9 +133,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||||
expression: &MatchExpression{
|
expression: &MatchExpression{
|
||||||
Expr: `header_regexp('foo')`,
|
Expr: `header_regexp('foo')`,
|
||||||
},
|
},
|
||||||
urlTarget: "https://example.com/foo",
|
wantErr: true,
|
||||||
httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "host matches localhost (MatchHost)",
|
name: "host matches localhost (MatchHost)",
|
||||||
|
@ -143,8 +164,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||||
expression: &MatchExpression{
|
expression: &MatchExpression{
|
||||||
Expr: `host(80)`,
|
Expr: `host(80)`,
|
||||||
},
|
},
|
||||||
urlTarget: "http://localhost:80",
|
wantErr: true,
|
||||||
wantErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "method does not match (MatchMethod)",
|
name: "method does not match (MatchMethod)",
|
||||||
|
@ -169,9 +189,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||||
expression: &MatchExpression{
|
expression: &MatchExpression{
|
||||||
Expr: `method()`,
|
Expr: `method()`,
|
||||||
},
|
},
|
||||||
urlTarget: "https://foo.example.com",
|
wantErr: true,
|
||||||
httpMethod: "PUT",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "path matches substring (MatchPath)",
|
name: "path matches substring (MatchPath)",
|
||||||
|
@ -266,24 +284,21 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
|
||||||
expression: &MatchExpression{
|
expression: &MatchExpression{
|
||||||
Expr: `protocol()`,
|
Expr: `protocol()`,
|
||||||
},
|
},
|
||||||
urlTarget: "https://example.com",
|
wantErr: true,
|
||||||
wantErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "protocol invocation error too many args (MatchProtocol)",
|
name: "protocol invocation error too many args (MatchProtocol)",
|
||||||
expression: &MatchExpression{
|
expression: &MatchExpression{
|
||||||
Expr: `protocol('grpc', 'https')`,
|
Expr: `protocol('grpc', 'https')`,
|
||||||
},
|
},
|
||||||
urlTarget: "https://example.com",
|
wantErr: true,
|
||||||
wantErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "protocol invocation error wrong arg type (MatchProtocol)",
|
name: "protocol invocation error wrong arg type (MatchProtocol)",
|
||||||
expression: &MatchExpression{
|
expression: &MatchExpression{
|
||||||
Expr: `protocol(true)`,
|
Expr: `protocol(true)`,
|
||||||
},
|
},
|
||||||
urlTarget: "https://example.com",
|
wantErr: true,
|
||||||
wantErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "query does not match against a specific value (MatchQuery)",
|
name: "query does not match against a specific value (MatchQuery)",
|
||||||
|
@ -330,40 +345,35 @@ 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,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "query error typed struct instead of map (MatchQuery)",
|
name: "query error typed struct instead of map (MatchQuery)",
|
||||||
expression: &MatchExpression{
|
expression: &MatchExpression{
|
||||||
Expr: `query(Message{field: "1"})`,
|
Expr: `query(Message{field: "1"})`,
|
||||||
},
|
},
|
||||||
urlTarget: "https://example.com/foo",
|
wantErr: true,
|
||||||
wantErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "query error bad map value type (MatchQuery)",
|
name: "query error bad map value type (MatchQuery)",
|
||||||
expression: &MatchExpression{
|
expression: &MatchExpression{
|
||||||
Expr: `query({"debug": 1})`,
|
Expr: `query({"debug": 1})`,
|
||||||
},
|
},
|
||||||
urlTarget: "https://example.com/foo/?debug=1",
|
wantErr: true,
|
||||||
wantErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "query error no args (MatchQuery)",
|
name: "query error no args (MatchQuery)",
|
||||||
expression: &MatchExpression{
|
expression: &MatchExpression{
|
||||||
Expr: `query()`,
|
Expr: `query()`,
|
||||||
},
|
},
|
||||||
urlTarget: "https://example.com/foo/?debug=1",
|
wantErr: true,
|
||||||
wantErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "remote_ip error no args (MatchRemoteIP)",
|
name: "remote_ip error no args (MatchRemoteIP)",
|
||||||
expression: &MatchExpression{
|
expression: &MatchExpression{
|
||||||
Expr: `remote_ip()`,
|
Expr: `remote_ip()`,
|
||||||
},
|
},
|
||||||
urlTarget: "https://example.com/foo",
|
wantErr: true,
|
||||||
wantErr: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "remote_ip single IP match (MatchRemoteIP)",
|
name: "remote_ip single IP match (MatchRemoteIP)",
|
||||||
|
@ -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 {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue