@marcel91200 / caddy Mirror · read-only / commits / 9cd472c0
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, 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")