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 {