@marcel91200 / caddy Mirror · read-only / commits / 792f1c7e
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 12, 07:00 UTC

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)

Francis Lavoie committed Oct 2, 2024 at 08:34 UTC 792f1c7ed759b97ee6dc80246cf2de054c09a12f
11 files changed +276 -58
.github/workflows/ci.yml
+25 -3
@@ -156,13 +156,35 @@ jobs:
156 # short sha is enough?
157 short_sha=$(git rev-parse --short HEAD)
158
159 + # To shorten the following lines
160 + ssh_opts="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
161 + ssh_host="$CI_USER@ci-s390x.caddyserver.com"
162 +
163 # The environment is fresh, so there's no point in keeping accepting and adding the key.
160 - 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"
161 - 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 ./..."
164 + rsync -arz -e "ssh $ssh_opts" --progress --delete --exclude '.git' . "$ssh_host":/var/tmp/"$short_sha"
165 + ssh $ssh_opts -t "$ssh_host" bash <<EOF
166 + cd /var/tmp/$short_sha
167 + go version
168 + go env
169 + printf "\n\n"
170 + retries=3
171 + exit_code=0
172 + while ((retries > 0)); do
173 + CGO_ENABLED=0 go test -p 1 -tags nobadger -v ./...
174 + exit_code=$?
175 + if ((exit_code == 0)); then
176 + break
177 + fi
178 + echo "\n\nTest failed: \$exit_code, retrying..."
179 + ((retries--))
180 + done
181 + echo "Remote exit code: \$exit_code"
182 + exit \$exit_code
183 + EOF
184 test_result=$?
185
186 # There's no need leaving the files around
165 - ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$CI_USER"@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
187 + ssh $ssh_opts "$ssh_host" "rm -rf /var/tmp/'$short_sha'"
188
189 echo "Test exit code: $test_result"
190 exit $test_result
.golangci.yml
+6
@@ -171,6 +171,12 @@ issues:
171 - path: modules/logging/filters.go
172 linters:
173 - dupl
174 + - path: modules/caddyhttp/matchers.go
175 + linters:
176 + - dupl
177 + - path: modules/caddyhttp/vars.go
178 + linters:
179 + - dupl
180 - path: _test\.go
181 linters:
182 - errcheck
go.mod
+1 -1
@@ -13,7 +13,7 @@ require (
13 github.com/caddyserver/zerossl v0.1.3
14 github.com/dustin/go-humanize v1.0.1
15 github.com/go-chi/chi/v5 v5.0.12
16 - github.com/google/cel-go v0.20.1
16 + github.com/google/cel-go v0.21.0
17 github.com/google/uuid v1.6.0
18 github.com/klauspost/compress v1.17.8
19 github.com/klauspost/cpuid/v2 v2.2.7
go.sum
+2 -2
@@ -198,8 +198,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
198 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
199 github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
200 github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
201 -github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
202 -github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
201 +github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
202 +github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
203 github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
204 github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
205 github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
modules/caddyhttp/celmatcher.go
+34 -17
@@ -126,6 +126,10 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
126 // light (and possibly naïve) syntactic sugar
127 m.expandedExpr = placeholderRegexp.ReplaceAllString(m.Expr, placeholderExpansion)
128
129 + // as a second pass, we'll strip the escape character from an escaped
130 + // placeholder, so that it can be used as an input to other CEL functions
131 + m.expandedExpr = escapedPlaceholderRegexp.ReplaceAllString(m.expandedExpr, escapedPlaceholderExpansion)
132 +
133 // our type adapter expands CEL's standard type support
134 m.ta = celTypeAdapter{}
135
@@ -159,14 +163,17 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
163
164 // create the CEL environment
165 env, err := cel.NewEnv(
162 - cel.Function(placeholderFuncName, cel.SingletonBinaryBinding(m.caddyPlaceholderFunc), cel.Overload(
163 - placeholderFuncName+"_httpRequest_string",
166 + cel.Function(CELPlaceholderFuncName, cel.SingletonBinaryBinding(m.caddyPlaceholderFunc), cel.Overload(
167 + CELPlaceholderFuncName+"_httpRequest_string",
168 []*cel.Type{httpRequestObjectType, cel.StringType},
169 cel.AnyType,
170 )),
167 - cel.Variable("request", httpRequestObjectType),
171 + cel.Variable(CELRequestVarName, httpRequestObjectType),
172 cel.CustomTypeAdapter(m.ta),
173 ext.Strings(),
174 + ext.Bindings(),
175 + ext.Lists(),
176 + ext.Math(),
177 matcherLib,
178 )
179 if err != nil {
@@ -247,7 +254,7 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
254 return types.NewErr(
255 "invalid request of type '%v' to %s(request, placeholderVarName)",
256 lhs.Type(),
250 - placeholderFuncName,
257 + CELPlaceholderFuncName,
258 )
259 }
260 phStr, ok := rhs.(types.String)
@@ -255,7 +262,7 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
262 return types.NewErr(
263 "invalid placeholder variable name of type '%v' to %s(request, placeholderVarName)",
264 rhs.Type(),
258 - placeholderFuncName,
265 + CELPlaceholderFuncName,
266 )
267 }
268
@@ -275,7 +282,7 @@ var httpRequestCELType = cel.ObjectType("http.Request", traits.ReceiverType)
282 type celHTTPRequest struct{ *http.Request }
283
284 func (cr celHTTPRequest) ResolveName(name string) (any, bool) {
278 - if name == "request" {
285 + if name == CELRequestVarName {
286 return cr, true
287 }
288 return nil, false
@@ -457,15 +464,15 @@ func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.Int
464 callArgs := call.Args()
465 reqAttr, ok := callArgs[0].(interpreter.InterpretableAttribute)
466 if !ok {
460 - return nil, errors.New("missing 'request' argument")
467 + return nil, errors.New("missing 'req' argument")
468 }
469 nsAttr, ok := reqAttr.Attr().(interpreter.NamespacedAttribute)
470 if !ok {
464 - return nil, errors.New("missing 'request' argument")
471 + return nil, errors.New("missing 'req' argument")
472 }
473 varNames := nsAttr.CandidateVariableNames()
467 - if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != "request" {
468 - return nil, errors.New("missing 'request' argument")
474 + if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != CELRequestVarName {
475 + return nil, errors.New("missing 'req' argument")
476 }
477 matcherData, ok := callArgs[1].(interpreter.InterpretableConst)
478 if !ok {
@@ -524,7 +531,7 @@ func celMatcherStringListMacroExpander(funcName string) cel.MacroFactory {
531 return nil, eh.NewError(arg.ID(), "matcher arguments must be string constants")
532 }
533 }
527 - return eh.NewCall(funcName, eh.NewIdent("request"), eh.NewList(matchArgs...)), nil
534 + return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), eh.NewList(matchArgs...)), nil
535 }
536 }
537
@@ -538,7 +545,7 @@ func celMatcherStringMacroExpander(funcName string) parser.MacroExpander {
545 return nil, eh.NewError(0, "matcher requires one argument")
546 }
547 if isCELStringExpr(args[0]) {
541 - return eh.NewCall(funcName, eh.NewIdent("request"), args[0]), nil
548 + return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), args[0]), nil
549 }
550 return nil, eh.NewError(args[0].ID(), "matcher argument must be a string literal")
551 }
@@ -572,7 +579,7 @@ func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander {
579 return nil, eh.NewError(entry.AsMapEntry().Value().ID(), "matcher map values must be string or list literals")
580 }
581 }
575 - return eh.NewCall(funcName, eh.NewIdent("request"), arg), nil
582 + return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), arg), nil
583 case ast.UnspecifiedExprKind, ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.SelectKind:
584 // appeasing the linter :)
585 }
@@ -646,7 +653,7 @@ func isCELCaddyPlaceholderCall(e ast.Expr) bool {
653 switch e.Kind() {
654 case ast.CallKind:
655 call := e.AsCall()
649 - if call.FunctionName() == "caddyPlaceholder" {
656 + if call.FunctionName() == CELPlaceholderFuncName {
657 return true
658 }
659 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 {
708 // expressions with a proper CEL function call; this is
709 // just for syntactic sugar.
710 var (
704 - placeholderRegexp = regexp.MustCompile(`{([a-zA-Z][\w.-]+)}`)
705 - placeholderExpansion = `caddyPlaceholder(request, "${1}")`
711 + // The placeholder may not be preceded by a backslash; the expansion
712 + // will include the preceding character if it is not a backslash.
713 + placeholderRegexp = regexp.MustCompile(`([^\\]|^){([a-zA-Z][\w.-]+)}`)
714 + placeholderExpansion = `${1}ph(req, "${2}")`
715 +
716 + // As a second pass, we need to strip the escape character in front of
717 + // the placeholder, if it exists.
718 + escapedPlaceholderRegexp = regexp.MustCompile(`\\{([a-zA-Z][\w.-]+)}`)
719 + escapedPlaceholderExpansion = `{${1}}`
720
721 CELTypeJSON = cel.MapType(cel.StringType, cel.DynType)
722 )
@@ -710,7 +724,10 @@ var (
724 var httpRequestObjectType = cel.ObjectType("http.Request")
725
726 // The name of the CEL function which accesses Replacer values.
713 -const placeholderFuncName = "caddyPlaceholder"
727 +const CELPlaceholderFuncName = "ph"
728 +
729 +// The name of the CEL request variable.
730 +const CELRequestVarName = "req"
731
732 const MatcherNameCtxKey = "matcher_name"
733
modules/caddyhttp/celmatcher_test.go
+105 -28
@@ -70,13 +70,36 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
70 wantResult: true,
71 },
72 {
73 - name: "header error (MatchHeader)",
73 + name: "header matches an escaped placeholder value (MatchHeader)",
74 expression: &MatchExpression{
75 - Expr: `header('foo')`,
75 + Expr: `header({'Field': '\\\{foobar}'})`,
76 },
77 urlTarget: "https://example.com/foo",
78 - httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
79 - wantErr: true,
78 + httpHeader: &http.Header{"Field": []string{"{foobar}"}},
79 + wantResult: true,
80 + },
81 + {
82 + name: "header matches an placeholder replaced during the header matcher (MatchHeader)",
83 + expression: &MatchExpression{
84 + Expr: `header({'Field': '\{http.request.uri.path}'})`,
85 + },
86 + urlTarget: "https://example.com/foo",
87 + httpHeader: &http.Header{"Field": []string{"/foo"}},
88 + wantResult: true,
89 + },
90 + {
91 + name: "header error, invalid escape sequence (MatchHeader)",
92 + expression: &MatchExpression{
93 + Expr: `header({'Field': '\\{foobar}'})`,
94 + },
95 + wantErr: true,
96 + },
97 + {
98 + name: "header error, needs to be JSON syntax with field as key (MatchHeader)",
99 + expression: &MatchExpression{
100 + Expr: `header('foo')`,
101 + },
102 + wantErr: true,
103 },
104 {
105 name: "header_regexp matches (MatchHeaderRE)",
@@ -110,9 +133,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
133 expression: &MatchExpression{
134 Expr: `header_regexp('foo')`,
135 },
113 - urlTarget: "https://example.com/foo",
114 - httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
115 - wantErr: true,
136 + wantErr: true,
137 },
138 {
139 name: "host matches localhost (MatchHost)",
@@ -143,8 +164,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
164 expression: &MatchExpression{
165 Expr: `host(80)`,
166 },
146 - urlTarget: "http://localhost:80",
147 - wantErr: true,
167 + wantErr: true,
168 },
169 {
170 name: "method does not match (MatchMethod)",
@@ -169,9 +189,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
189 expression: &MatchExpression{
190 Expr: `method()`,
191 },
172 - urlTarget: "https://foo.example.com",
173 - httpMethod: "PUT",
174 - wantErr: true,
192 + wantErr: true,
193 },
194 {
195 name: "path matches substring (MatchPath)",
@@ -266,24 +284,21 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
284 expression: &MatchExpression{
285 Expr: `protocol()`,
286 },
269 - urlTarget: "https://example.com",
270 - wantErr: true,
287 + wantErr: true,
288 },
289 {
290 name: "protocol invocation error too many args (MatchProtocol)",
291 expression: &MatchExpression{
292 Expr: `protocol('grpc', 'https')`,
293 },
277 - urlTarget: "https://example.com",
278 - wantErr: true,
294 + wantErr: true,
295 },
296 {
297 name: "protocol invocation error wrong arg type (MatchProtocol)",
298 expression: &MatchExpression{
299 Expr: `protocol(true)`,
300 },
285 - urlTarget: "https://example.com",
286 - wantErr: true,
301 + wantErr: true,
302 },
303 {
304 name: "query does not match against a specific value (MatchQuery)",
@@ -330,40 +345,35 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
345 expression: &MatchExpression{
346 Expr: `query({1: "1"})`,
347 },
333 - urlTarget: "https://example.com/foo",
334 - wantErr: true,
348 + wantErr: true,
349 },
350 {
351 name: "query error typed struct instead of map (MatchQuery)",
352 expression: &MatchExpression{
353 Expr: `query(Message{field: "1"})`,
354 },
341 - urlTarget: "https://example.com/foo",
342 - wantErr: true,
355 + wantErr: true,
356 },
357 {
358 name: "query error bad map value type (MatchQuery)",
359 expression: &MatchExpression{
360 Expr: `query({"debug": 1})`,
361 },
349 - urlTarget: "https://example.com/foo/?debug=1",
350 - wantErr: true,
362 + wantErr: true,
363 },
364 {
365 name: "query error no args (MatchQuery)",
366 expression: &MatchExpression{
367 Expr: `query()`,
368 },
357 - urlTarget: "https://example.com/foo/?debug=1",
358 - wantErr: true,
369 + wantErr: true,
370 },
371 {
372 name: "remote_ip error no args (MatchRemoteIP)",
373 expression: &MatchExpression{
374 Expr: `remote_ip()`,
375 },
365 - urlTarget: "https://example.com/foo",
366 - wantErr: true,
376 + wantErr: true,
377 },
378 {
379 name: "remote_ip single IP match (MatchRemoteIP)",
@@ -373,6 +383,67 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
383 urlTarget: "https://example.com/foo",
384 wantResult: true,
385 },
386 + {
387 + name: "vars value (VarsMatcher)",
388 + expression: &MatchExpression{
389 + Expr: `vars({'foo': 'bar'})`,
390 + },
391 + urlTarget: "https://example.com/foo",
392 + wantResult: true,
393 + },
394 + {
395 + name: "vars matches placeholder, needs escape (VarsMatcher)",
396 + expression: &MatchExpression{
397 + Expr: `vars({'\{http.request.uri.path}': '/foo'})`,
398 + },
399 + urlTarget: "https://example.com/foo",
400 + wantResult: true,
401 + },
402 + {
403 + name: "vars error wrong syntax (VarsMatcher)",
404 + expression: &MatchExpression{
405 + Expr: `vars('foo', 'bar')`,
406 + },
407 + wantErr: true,
408 + },
409 + {
410 + name: "vars error no args (VarsMatcher)",
411 + expression: &MatchExpression{
412 + Expr: `vars()`,
413 + },
414 + wantErr: true,
415 + },
416 + {
417 + name: "vars_regexp value (MatchVarsRE)",
418 + expression: &MatchExpression{
419 + Expr: `vars_regexp('foo', 'ba?r')`,
420 + },
421 + urlTarget: "https://example.com/foo",
422 + wantResult: true,
423 + },
424 + {
425 + name: "vars_regexp value with name (MatchVarsRE)",
426 + expression: &MatchExpression{
427 + Expr: `vars_regexp('name', 'foo', 'ba?r')`,
428 + },
429 + urlTarget: "https://example.com/foo",
430 + wantResult: true,
431 + },
432 + {
433 + name: "vars_regexp matches placeholder, needs escape (MatchVarsRE)",
434 + expression: &MatchExpression{
435 + Expr: `vars_regexp('\{http.request.uri.path}', '/fo?o')`,
436 + },
437 + urlTarget: "https://example.com/foo",
438 + wantResult: true,
439 + },
440 + {
441 + name: "vars_regexp error no args (MatchVarsRE)",
442 + expression: &MatchExpression{
443 + Expr: `vars_regexp()`,
444 + },
445 + wantErr: true,
446 + },
447 }
448 )
449
@@ -396,6 +467,9 @@ func TestMatchExpressionMatch(t *testing.T) {
467 }
468 repl := caddy.NewReplacer()
469 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
470 + ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
471 + "foo": "bar",
472 + })
473 req = req.WithContext(ctx)
474 addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
475
@@ -436,6 +510,9 @@ func BenchmarkMatchExpressionMatch(b *testing.B) {
510 }
511 repl := caddy.NewReplacer()
512 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
513 + ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
514 + "foo": "bar",
515 + })
516 req = req.WithContext(ctx)
517 addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
518 if tc.clientCertificate != nil {
modules/caddyhttp/fileserver/matcher.go
+5 -5
@@ -225,7 +225,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
225 return func(eh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) {
226 if len(args) == 0 {
227 return eh.NewCall("file",
228 - eh.NewIdent("request"),
228 + eh.NewIdent(caddyhttp.CELRequestVarName),
229 eh.NewMap(),
230 ), nil
231 }
@@ -233,7 +233,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
233 arg := args[0]
234 if isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg) {
235 return eh.NewCall("file",
236 - eh.NewIdent("request"),
236 + eh.NewIdent(caddyhttp.CELRequestVarName),
237 eh.NewMap(eh.NewMapEntry(
238 eh.NewLiteral(types.String("try_files")),
239 eh.NewList(arg),
@@ -242,7 +242,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
242 ), nil
243 }
244 if isCELTryFilesLiteral(arg) {
245 - return eh.NewCall("file", eh.NewIdent("request"), arg), nil
245 + return eh.NewCall("file", eh.NewIdent(caddyhttp.CELRequestVarName), arg), nil
246 }
247 return nil, &common.Error{
248 Location: eh.OffsetLocation(arg.ID()),
@@ -259,7 +259,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
259 }
260 }
261 return eh.NewCall("file",
262 - eh.NewIdent("request"),
262 + eh.NewIdent(caddyhttp.CELRequestVarName),
263 eh.NewMap(eh.NewMapEntry(
264 eh.NewLiteral(types.String("try_files")),
265 eh.NewList(args...),
@@ -634,7 +634,7 @@ func isCELCaddyPlaceholderCall(e ast.Expr) bool {
634 switch e.Kind() {
635 case ast.CallKind:
636 call := e.AsCall()
637 - if call.FunctionName() == "caddyPlaceholder" {
637 + if call.FunctionName() == caddyhttp.CELPlaceholderFuncName {
638 return true
639 }
640 case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind:
modules/caddyhttp/matchers.go
+2 -2
@@ -1562,8 +1562,8 @@ var (
1562 _ CELLibraryProducer = (*MatchHeader)(nil)
1563 _ CELLibraryProducer = (*MatchHeaderRE)(nil)
1564 _ CELLibraryProducer = (*MatchProtocol)(nil)
1565 - // _ CELLibraryProducer = (*VarsMatcher)(nil)
1566 - // _ CELLibraryProducer = (*MatchVarsRE)(nil)
1565 + _ CELLibraryProducer = (*VarsMatcher)(nil)
1566 + _ CELLibraryProducer = (*MatchVarsRE)(nil)
1567
1568 _ json.Marshaler = (*MatchNot)(nil)
1569 _ json.Unmarshaler = (*MatchNot)(nil)
modules/caddyhttp/templates/templates.go
+6
@@ -81,6 +81,12 @@ func init() {
81 // {{placeholder "http.error.status_code"}}
82 // ```
83 //
84 +// As a shortcut, `ph` is an alias for `placeholder`.
85 +//
86 +// ```
87 +// {{ph "http.request.method"}}
88 +// ```
89 +//
90 // ##### `.Host`
91 //
92 // Returns the hostname portion (no port) of the Host header of the HTTP request.
modules/caddyhttp/templates/tplcontext.go
+1
@@ -88,6 +88,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
88 "fileStat": c.funcFileStat,
89 "env": c.funcEnv,
90 "placeholder": c.funcPlaceholder,
91 + "ph": c.funcPlaceholder, // shortcut
92 "fileExists": c.funcFileExists,
93 "httpError": c.funcHTTPError,
94 "humanize": c.funcHumanize,
modules/caddyhttp/vars.go
+89
@@ -18,8 +18,12 @@ import (
18 "context"
19 "fmt"
20 "net/http"
21 + "reflect"
22 "strings"
23
24 + "github.com/google/cel-go/cel"
25 + "github.com/google/cel-go/common/types/ref"
26 +
27 "github.com/caddyserver/caddy/v2"
28 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
29 )
@@ -203,6 +207,28 @@ func (m VarsMatcher) Match(r *http.Request) bool {
207 return false
208 }
209
210 +// CELLibrary produces options that expose this matcher for use in CEL
211 +// expression matchers.
212 +//
213 +// Example:
214 +//
215 +// expression vars({'{magic_number}': ['3', '5']})
216 +// expression vars({'{foo}': 'single_value'})
217 +func (VarsMatcher) CELLibrary(_ caddy.Context) (cel.Library, error) {
218 + return CELMatcherImpl(
219 + "vars",
220 + "vars_matcher_request_map",
221 + []*cel.Type{CELTypeJSON},
222 + func(data ref.Val) (RequestMatcher, error) {
223 + mapStrListStr, err := CELValueToMapStrList(data)
224 + if err != nil {
225 + return nil, err
226 + }
227 + return VarsMatcher(mapStrListStr), nil
228 + },
229 + )
230 +}
231 +
232 // MatchVarsRE matches the value of the context variables by a given regular expression.
233 //
234 // 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 {
328 return false
329 }
330
331 +// CELLibrary produces options that expose this matcher for use in CEL
332 +// expression matchers.
333 +//
334 +// Example:
335 +//
336 +// expression vars_regexp('foo', '{magic_number}', '[0-9]+')
337 +// expression vars_regexp('{magic_number}', '[0-9]+')
338 +func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
339 + unnamedPattern, err := CELMatcherImpl(
340 + "vars_regexp",
341 + "vars_regexp_request_string_string",
342 + []*cel.Type{cel.StringType, cel.StringType},
343 + func(data ref.Val) (RequestMatcher, error) {
344 + refStringList := reflect.TypeOf([]string{})
345 + params, err := data.ConvertToNative(refStringList)
346 + if err != nil {
347 + return nil, err
348 + }
349 + strParams := params.([]string)
350 + matcher := MatchVarsRE{}
351 + matcher[strParams[0]] = &MatchRegexp{
352 + Pattern: strParams[1],
353 + Name: ctx.Value(MatcherNameCtxKey).(string),
354 + }
355 + err = matcher.Provision(ctx)
356 + return matcher, err
357 + },
358 + )
359 + if err != nil {
360 + return nil, err
361 + }
362 + namedPattern, err := CELMatcherImpl(
363 + "vars_regexp",
364 + "vars_regexp_request_string_string_string",
365 + []*cel.Type{cel.StringType, cel.StringType, cel.StringType},
366 + func(data ref.Val) (RequestMatcher, error) {
367 + refStringList := reflect.TypeOf([]string{})
368 + params, err := data.ConvertToNative(refStringList)
369 + if err != nil {
370 + return nil, err
371 + }
372 + strParams := params.([]string)
373 + name := strParams[0]
374 + if name == "" {
375 + name = ctx.Value(MatcherNameCtxKey).(string)
376 + }
377 + matcher := MatchVarsRE{}
378 + matcher[strParams[1]] = &MatchRegexp{
379 + Pattern: strParams[2],
380 + Name: name,
381 + }
382 + err = matcher.Provision(ctx)
383 + return matcher, err
384 + },
385 + )
386 + if err != nil {
387 + return nil, err
388 + }
389 + envOpts := append(unnamedPattern.CompileOptions(), namedPattern.CompileOptions()...)
390 + prgOpts := append(unnamedPattern.ProgramOptions(), namedPattern.ProgramOptions()...)
391 + return NewMatcherCELLibrary(envOpts, prgOpts), nil
392 +}
393 +
394 // Validate validates m's regular expressions.
395 func (m MatchVarsRE) Validate() error {
396 for _, rm := range m {