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, 14:30 UTC
caddyfile: Populate regexp matcher names by default (#6145)
* caddyfile: Populate regexp matcher names by default * Some lint cleanup that my VSCode complained about * Pass down matcher name through expression matcher * Compat with #6113: fix adapt test, set both styles in replacer
Francis Lavoie committed
Apr 17, 2024 at 14:19 UTC
9cd472c0313b01e71d1f142769c3653058d75c86
14 files changed
+183
-27
caddyconfig/caddyfile/dispenser.go
+34
@@ -30,6 +30,10 @@ type Dispenser struct {
30
tokens []Token
31
cursor int
32
nesting int
33
+
34
+ // A map of arbitrary context data that can be used
35
+ // to pass through some information to unmarshalers.
36
+ context map[string]any
37
}
38
39
// NewDispenser returns a Dispenser filled with the given tokens.
@@ -454,6 +458,34 @@ func (d *Dispenser) DeleteN(amount int) []Token {
458
return d.tokens
459
}
460
461
+// SetContext sets a key-value pair in the context map.
462
+func (d *Dispenser) SetContext(key string, value any) {
463
+ if d.context == nil {
464
+ d.context = make(map[string]any)
465
+ }
466
+ d.context[key] = value
467
+}
468
+
469
+// GetContext gets the value of a key in the context map.
470
+func (d *Dispenser) GetContext(key string) any {
471
+ if d.context == nil {
472
+ return nil
473
+ }
474
+ return d.context[key]
475
+}
476
+
477
+// GetContextString gets the value of a key in the context map
478
+// as a string, or an empty string if the key does not exist.
479
+func (d *Dispenser) GetContextString(key string) string {
480
+ if d.context == nil {
481
+ return ""
482
+ }
483
+ if val, ok := d.context[key].(string); ok {
484
+ return val
485
+ }
486
+ return ""
487
+}
488
+
489
// isNewLine determines whether the current token is on a different
490
// line (higher line number) than the previous token. It handles imported
491
// tokens correctly. If there isn't a previous token, it returns true.
@@ -485,3 +517,5 @@ func (d *Dispenser) isNextOnNewLine() bool {
517
next := d.tokens[d.cursor+1]
518
return isNextOnNewLine(curr, next)
519
}
520
+
521
+const MatcherNameCtxKey = "matcher_name"
caddyconfig/httpcaddyfile/httptype.go
+9
-1
@@ -1397,6 +1397,14 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
1397
// given a matcher name and the tokens following it, parse
1398
// the tokens as a matcher module and record it
1399
makeMatcher := func(matcherName string, tokens []caddyfile.Token) error {
1400
+ // create a new dispenser from the tokens
1401
+ dispenser := caddyfile.NewDispenser(tokens)
1402
+
1403
+ // set the matcher name (without @) in the dispenser context so
1404
+ // that matcher modules can access it to use it as their name
1405
+ // (e.g. regexp matchers which use the name for capture groups)
1406
+ dispenser.SetContext(caddyfile.MatcherNameCtxKey, definitionName[1:])
1407
+
1408
mod, err := caddy.GetModule("http.matchers." + matcherName)
1409
if err != nil {
1410
return fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
@@ -1405,7 +1413,7 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
1413
if !ok {
1414
return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
1415
}
1408
- err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens))
1416
+ err = unm.UnmarshalCaddyfile(dispenser)
1417
if err != nil {
1418
return err
1419
}
caddyconfig/httpcaddyfile/serveroptions.go
+1
-1
@@ -291,7 +291,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
291
func applyServerOptions(
292
servers map[string]*caddyhttp.Server,
293
options map[string]any,
294
- warnings *[]caddyconfig.Warning,
294
+ _ *[]caddyconfig.Warning,
295
) error {
296
serverOpts, ok := options["servers"].([]serverOptions)
297
if !ok {
caddyconfig/httpcaddyfile/tlsapp.go
+5
-1
@@ -487,7 +487,11 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
487
// for any other automation policies. A nil policy (and no error) will be
488
// returned if there are no default/global options. However, if always is
489
// true, a non-nil value will always be returned (unless there is an error).
490
-func newBaseAutomationPolicy(options map[string]any, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
490
+func newBaseAutomationPolicy(
491
+ options map[string]any,
492
+ _ []caddyconfig.Warning,
493
+ always bool,
494
+) (*caddytls.AutomationPolicy, error) {
495
issuers, hasIssuers := options["cert_issuer"]
496
_, hasLocalCerts := options["local_certs"]
497
keyType, hasKeyType := options["key_type"]
caddytest/integration/caddyfile_adapt/expression_quotes.caddyfiletest
+8
-2
@@ -84,7 +84,10 @@ abort @e
84
],
85
"match": [
86
{
87
- "expression": "{http.error.status_code} == 403"
87
+ "expression": {
88
+ "expr": "{http.error.status_code} == 403",
89
+ "name": "d"
90
+ }
91
}
92
]
93
},
@@ -97,7 +100,10 @@ abort @e
100
],
101
"match": [
102
{
100
- "expression": "{http.error.status_code} == 404"
103
+ "expression": {
104
+ "expr": "{http.error.status_code} == 404",
105
+ "name": "e"
106
+ }
107
}
108
]
109
}
caddytest/integration/caddyfile_adapt/matcher_syntax.caddyfiletest
+5
-1
@@ -146,6 +146,7 @@
146
{
147
"vars_regexp": {
148
"{http.request.uri}": {
149
+ "name": "matcher6",
150
"pattern": "\\.([a-f0-9]{6})\\.(css|js)$"
151
}
152
}
@@ -161,7 +162,10 @@
162
{
163
"match": [
164
{
164
- "expression": "path('/foo*') \u0026\u0026 method('GET')"
165
+ "expression": {
166
+ "expr": "path('/foo*') \u0026\u0026 method('GET')",
167
+ "name": "matcher7"
168
+ }
169
}
170
],
171
"handle": [
caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.caddyfiletest
+1
@@ -36,6 +36,7 @@ respond @match "{re.1}"
36
"match": [
37
{
38
"path_regexp": {
39
+ "name": "match",
40
"pattern": "^/foo(.*)$"
41
}
42
}
context.go
+12
@@ -556,3 +556,15 @@ func (ctx Context) Module() Module {
556
}
557
return ctx.ancestry[len(ctx.ancestry)-1]
558
}
559
+
560
+// WithValue returns a new context with the given key-value pair.
561
+func (ctx *Context) WithValue(key, value any) Context {
562
+ return Context{
563
+ Context: context.WithValue(ctx.Context, key, value),
564
+ moduleInstances: ctx.moduleInstances,
565
+ cfg: ctx.cfg,
566
+ ancestry: ctx.ancestry,
567
+ cleanupFuncs: ctx.cleanupFuncs,
568
+ exitFuncs: ctx.exitFuncs,
569
+ }
570
+}
modules/caddyhttp/celmatcher.go
+44
-3
@@ -62,7 +62,12 @@ type MatchExpression struct {
62
// The CEL expression to evaluate. Any Caddy placeholders
63
// will be expanded and situated into proper CEL function
64
// calls before evaluating.
65
- Expr string
65
+ Expr string `json:"expr,omitempty"`
66
+
67
+ // Name is an optional name for this matcher.
68
+ // This is used to populate the name for regexp
69
+ // matchers that appear in the expression.
70
+ Name string `json:"name,omitempty"`
71
72
expandedExpr string
73
prg cel.Program
@@ -81,12 +86,36 @@ func (MatchExpression) CaddyModule() caddy.ModuleInfo {
86
87
// MarshalJSON marshals m's expression.
88
func (m MatchExpression) MarshalJSON() ([]byte, error) {
84
- return json.Marshal(m.Expr)
89
+ // if the name is empty, then we can marshal just the expression string
90
+ if m.Name == "" {
91
+ return json.Marshal(m.Expr)
92
+ }
93
+ // otherwise, we need to marshal the full object, using an
94
+ // anonymous struct to avoid infinite recursion
95
+ return json.Marshal(struct {
96
+ Expr string `json:"expr"`
97
+ Name string `json:"name"`
98
+ }{
99
+ Expr: m.Expr,
100
+ Name: m.Name,
101
+ })
102
}
103
104
// UnmarshalJSON unmarshals m's expression.
105
func (m *MatchExpression) UnmarshalJSON(data []byte) error {
89
- return json.Unmarshal(data, &m.Expr)
106
+ // if the data is a string, then it's just the expression
107
+ if data[0] == '"' {
108
+ return json.Unmarshal(data, &m.Expr)
109
+ }
110
+ // otherwise, it's a full object, so unmarshal it,
111
+ // using an temp map to avoid infinite recursion
112
+ var tmpJson map[string]any
113
+ err := json.Unmarshal(data, &tmpJson)
114
+ *m = MatchExpression{
115
+ Expr: tmpJson["expr"].(string),
116
+ Name: tmpJson["name"].(string),
117
+ }
118
+ return err
119
}
120
121
// Provision sets ups m.
@@ -109,6 +138,11 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
138
matcherLibProducers = append(matcherLibProducers, p)
139
}
140
}
141
+
142
+ // add the matcher name to the context so that the matcher name
143
+ // can be used by regexp matchers being provisioned
144
+ ctx = ctx.WithValue(MatcherNameCtxKey, m.Name)
145
+
146
// Assemble the compilation and program options from the different library
147
// producers into a single cel.Library implementation.
148
matcherEnvOpts := []cel.EnvOption{}
@@ -197,6 +231,11 @@ func (m *MatchExpression) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
231
// quoted string; commonly quotes are used in Caddyfile to
232
// define the expression
233
m.Expr = d.Val()
234
+
235
+ // use the named matcher's name, to fill regexp
236
+ // matchers names by default
237
+ m.Name = d.GetContextString(caddyfile.MatcherNameCtxKey)
238
+
239
return nil
240
}
241
@@ -673,6 +712,8 @@ var httpRequestObjectType = cel.ObjectType("http.Request")
712
// The name of the CEL function which accesses Replacer values.
713
const placeholderFuncName = "caddyPlaceholder"
714
715
+const MatcherNameCtxKey = "matcher_name"
716
+
717
// Interface guards
718
var (
719
_ caddy.Provisioner = (*MatchExpression)(nil)
modules/caddyhttp/celmatcher_test.go
+6
-2
@@ -380,7 +380,9 @@ func TestMatchExpressionMatch(t *testing.T) {
380
for _, tst := range matcherTests {
381
tc := tst
382
t.Run(tc.name, func(t *testing.T) {
383
- err := tc.expression.Provision(caddy.Context{})
383
+ caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
384
+ defer cancel()
385
+ err := tc.expression.Provision(caddyCtx)
386
if err != nil {
387
if !tc.wantErr {
388
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr)
@@ -482,7 +484,9 @@ func TestMatchExpressionProvision(t *testing.T) {
484
}
485
for _, tt := range tests {
486
t.Run(tt.name, func(t *testing.T) {
485
- if err := tt.expression.Provision(caddy.Context{}); (err != nil) != tt.wantErr {
487
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
488
+ defer cancel()
489
+ if err := tt.expression.Provision(ctx); (err != nil) != tt.wantErr {
490
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tt.wantErr)
491
}
492
})
modules/caddyhttp/fileserver/matcher_test.go
+3
-1
@@ -360,7 +360,9 @@ func TestMatchExpressionMatch(t *testing.T) {
360
for _, tst := range expressionTests {
361
tc := tst
362
t.Run(tc.name, func(t *testing.T) {
363
- err := tc.expression.Provision(caddy.Context{})
363
+ caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
364
+ defer cancel()
365
+ err := tc.expression.Provision(caddyCtx)
366
if err != nil {
367
if !tc.wantErr {
368
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr)
modules/caddyhttp/matchers.go
+49
-14
@@ -675,7 +675,10 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
675
[]*cel.Type{cel.StringType},
676
func(data ref.Val) (RequestMatcher, error) {
677
pattern := data.(types.String)
678
- matcher := MatchPathRE{MatchRegexp{Pattern: string(pattern)}}
678
+ matcher := MatchPathRE{MatchRegexp{
679
+ Name: ctx.Value(MatcherNameCtxKey).(string),
680
+ Pattern: string(pattern),
681
+ }}
682
err := matcher.Provision(ctx)
683
return matcher, err
684
},
@@ -694,7 +697,14 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
697
return nil, err
698
}
699
strParams := params.([]string)
697
- matcher := MatchPathRE{MatchRegexp{Name: strParams[0], Pattern: strParams[1]}}
700
+ name := strParams[0]
701
+ if name == "" {
702
+ name = ctx.Value(MatcherNameCtxKey).(string)
703
+ }
704
+ matcher := MatchPathRE{MatchRegexp{
705
+ Name: name,
706
+ Pattern: strParams[1],
707
+ }}
708
err = matcher.Provision(ctx)
709
return matcher, err
710
},
@@ -1023,6 +1033,11 @@ func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1033
val = second
1034
}
1035
1036
+ // Default to the named matcher's name, if no regexp name is provided
1037
+ if name == "" {
1038
+ name = d.GetContextString(caddyfile.MatcherNameCtxKey)
1039
+ }
1040
+
1041
// If there's already a pattern for this field
1042
// then we would end up overwriting the old one
1043
if (*m)[field] != nil {
@@ -1099,7 +1114,10 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
1114
}
1115
strParams := params.([]string)
1116
matcher := MatchHeaderRE{}
1102
- matcher[strParams[0]] = &MatchRegexp{Pattern: strParams[1], Name: ""}
1117
+ matcher[strParams[0]] = &MatchRegexp{
1118
+ Pattern: strParams[1],
1119
+ Name: ctx.Value(MatcherNameCtxKey).(string),
1120
+ }
1121
err = matcher.Provision(ctx)
1122
return matcher, err
1123
},
@@ -1118,8 +1136,15 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
1136
return nil, err
1137
}
1138
strParams := params.([]string)
1139
+ name := strParams[0]
1140
+ if name == "" {
1141
+ name = ctx.Value(MatcherNameCtxKey).(string)
1142
+ }
1143
matcher := MatchHeaderRE{}
1122
- matcher[strParams[1]] = &MatchRegexp{Pattern: strParams[2], Name: strParams[0]}
1144
+ matcher[strParams[1]] = &MatchRegexp{
1145
+ Pattern: strParams[2],
1146
+ Name: name,
1147
+ }
1148
err = matcher.Provision(ctx)
1149
return matcher, err
1150
},
@@ -1284,7 +1309,6 @@ type MatchRegexp struct {
1309
Pattern string `json:"pattern"`
1310
1311
compiled *regexp.Regexp
1287
- phPrefix string
1312
}
1313
1314
// Provision compiles the regular expression.
@@ -1294,10 +1318,6 @@ func (mre *MatchRegexp) Provision(caddy.Context) error {
1318
return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
1319
}
1320
mre.compiled = re
1297
- mre.phPrefix = regexpPlaceholderPrefix
1298
- if mre.Name != "" {
1299
- mre.phPrefix += "." + mre.Name
1300
- }
1321
return nil
1322
}
1323
@@ -1321,16 +1341,25 @@ func (mre *MatchRegexp) Match(input string, repl *caddy.Replacer) bool {
1341
1342
// save all capture groups, first by index
1343
for i, match := range matches {
1324
- key := mre.phPrefix + "." + strconv.Itoa(i)
1325
- repl.Set(key, match)
1344
+ keySuffix := "." + strconv.Itoa(i)
1345
+ if mre.Name != "" {
1346
+ repl.Set(regexpPlaceholderPrefix+"."+mre.Name+keySuffix, match)
1347
+ }
1348
+ repl.Set(regexpPlaceholderPrefix+keySuffix, match)
1349
}
1350
1351
// then by name
1352
for i, name := range mre.compiled.SubexpNames() {
1330
- if i != 0 && name != "" {
1331
- key := mre.phPrefix + "." + name
1332
- repl.Set(key, matches[i])
1353
+ // skip the first element (the full match), and empty names
1354
+ if i == 0 || name == "" {
1355
+ continue
1356
+ }
1357
+
1358
+ keySuffix := "." + name
1359
+ if mre.Name != "" {
1360
+ repl.Set(regexpPlaceholderPrefix+"."+mre.Name+keySuffix, matches[i])
1361
}
1362
+ repl.Set(regexpPlaceholderPrefix+keySuffix, matches[i])
1363
}
1364
1365
return true
@@ -1357,6 +1386,12 @@ func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1386
default:
1387
return d.ArgErr()
1388
}
1389
+
1390
+ // Default to the named matcher's name, if no regexp name is provided
1391
+ if mre.Name == "" {
1392
+ mre.Name = d.GetContextString(caddyfile.MatcherNameCtxKey)
1393
+ }
1394
+
1395
if d.NextBlock(0) {
1396
return d.Err("malformed path_regexp matcher: blocks are not supported")
1397
}
modules/caddyhttp/routes.go
+1
-1
@@ -311,7 +311,7 @@ func wrapRoute(route Route) Middleware {
311
// we need to pull this particular MiddlewareHandler
312
// pointer into its own stack frame to preserve it so it
313
// won't be overwritten in future loop iterations.
314
-func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) Middleware {
314
+func wrapMiddleware(_ caddy.Context, mh MiddlewareHandler, metrics *Metrics) Middleware {
315
handlerToUse := mh
316
if metrics != nil {
317
// wrap the middleware with metrics instrumentation
modules/caddyhttp/vars.go
+5
@@ -242,6 +242,11 @@ func (m *MatchVarsRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
242
val = second
243
}
244
245
+ // Default to the named matcher's name, if no regexp name is provided
246
+ if name == "" {
247
+ name = d.GetContextString(caddyfile.MatcherNameCtxKey)
248
+ }
249
+
250
(*m)[field] = &MatchRegexp{Pattern: val, Name: name}
251
if d.NextBlock(0) {
252
return d.Err("malformed vars_regexp matcher: blocks are not supported")