@marcel91200 / caddy Mirror · read-only / commits / 09b2cbcf
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: Add `MatchWithError` to replace SetVar hack (#6596)

* caddyhttp: Add `MatchWithError` to replace SetVar hack * Error in IP matchers on TLS handshake not complete * Use MatchWithError everywhere possible * Move implementations to MatchWithError versions * Looser interface checking to allow fallback * CEL factories can return RequestMatcherWithError * Clarifying comment since it's subtle that an err is returned * Return 425 Too Early status in IP matchers * Keep AnyMatch signature the same for now * Apparently Deprecated can't be all-uppercase to get IDE linting * Linter

Francis Lavoie committed Nov 4, 2024 at 18:18 UTC 09b2cbcf4d839adec91b189fea549d64a69e0595
16 files changed +536 -202
caddyconfig/httpcaddyfile/httptype.go
+13 -8
@@ -1466,9 +1466,9 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
1466
1467 // iterate each pairing of host and path matchers and
1468 // put them into a map for JSON encoding
1469 - var matcherSets []map[string]caddyhttp.RequestMatcher
1469 + var matcherSets []map[string]caddyhttp.RequestMatcherWithError
1470 for _, mp := range matcherPairs {
1471 - matcherSet := make(map[string]caddyhttp.RequestMatcher)
1471 + matcherSet := make(map[string]caddyhttp.RequestMatcherWithError)
1472 if len(mp.hostm) > 0 {
1473 matcherSet["host"] = mp.hostm
1474 }
@@ -1527,12 +1527,17 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
1527 if err != nil {
1528 return err
1529 }
1530 - rm, ok := unm.(caddyhttp.RequestMatcher)
1531 - if !ok {
1532 - return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
1530 +
1531 + if rm, ok := unm.(caddyhttp.RequestMatcherWithError); ok {
1532 + matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
1533 + return nil
1534 }
1534 - matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
1535 - return nil
1535 + // nolint:staticcheck
1536 + if rm, ok := unm.(caddyhttp.RequestMatcher); ok {
1537 + matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
1538 + return nil
1539 + }
1540 + return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
1541 }
1542
1543 // if the next token is quoted, we can assume it's not a matcher name
@@ -1576,7 +1581,7 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
1581 return nil
1582 }
1583
1579 -func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.ModuleMap, error) {
1584 +func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcherWithError) (caddy.ModuleMap, error) {
1585 msEncoded := make(caddy.ModuleMap)
1586 for matcherName, val := range matchers {
1587 jsonBytes, err := json.Marshal(val)
modules/caddyhttp/caddyhttp.go
+16
@@ -36,10 +36,26 @@ func init() {
36 // RequestMatcher is a type that can match to a request.
37 // A route matcher MUST NOT modify the request, with the
38 // only exception being its context.
39 +//
40 +// Deprecated: Matchers should now implement RequestMatcherWithError.
41 +// You may remove any interface guards for RequestMatcher
42 +// but keep your Match() methods for backwards compatibility.
43 type RequestMatcher interface {
44 Match(*http.Request) bool
45 }
46
47 +// RequestMatcherWithError is like RequestMatcher but can return an error.
48 +// An error during matching will abort the request middleware chain and
49 +// invoke the error middleware chain.
50 +//
51 +// This will eventually replace RequestMatcher. Matcher modules
52 +// should implement both interfaces, and once all modules have
53 +// been updated to use RequestMatcherWithError, the RequestMatcher
54 +// interface may eventually be dropped.
55 +type RequestMatcherWithError interface {
56 + MatchWithError(*http.Request) (bool, error)
57 +}
58 +
59 // Handler is like http.Handler except ServeHTTP may return an error.
60 //
61 // If any handler encounters an error, it should be returned for proper
modules/caddyhttp/celmatcher.go
+102 -33
@@ -202,17 +202,25 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
202
203 // Match returns true if r matches m.
204 func (m MatchExpression) Match(r *http.Request) bool {
205 + match, err := m.MatchWithError(r)
206 + if err != nil {
207 + SetVar(r.Context(), MatcherErrorVarKey, err)
208 + }
209 + return match
210 +}
211 +
212 +// MatchWithError returns true if r matches m.
213 +func (m MatchExpression) MatchWithError(r *http.Request) (bool, error) {
214 celReq := celHTTPRequest{r}
215 out, _, err := m.prg.Eval(celReq)
216 if err != nil {
217 m.log.Error("evaluating expression", zap.Error(err))
209 - SetVar(r.Context(), MatcherErrorVarKey, err)
210 - return false
218 + return false, err
219 }
220 if outBool, ok := out.Value().(bool); ok {
213 - return outBool
221 + return outBool, nil
222 }
215 - return false
223 + return false, nil
224 }
225
226 // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
@@ -380,7 +388,7 @@ type CELLibraryProducer interface {
388 // limited set of function signatures. For strong type validation you may need
389 // to provide a custom macro which does a more detailed analysis of the CEL
390 // literal provided to the macro as an argument.
383 -func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fac CELMatcherFactory) (cel.Library, error) {
391 +func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fac any) (cel.Library, error) {
392 requestType := cel.ObjectType("http.Request")
393 var macro parser.Macro
394 switch len(matcherDataTypes) {
@@ -424,7 +432,11 @@ func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fa
432 }
433
434 // CELMatcherFactory converts a constant CEL value into a RequestMatcher.
427 -type CELMatcherFactory func(data ref.Val) (RequestMatcher, error)
435 +// Deprecated: Use CELMatcherWithErrorFactory instead.
436 +type CELMatcherFactory = func(data ref.Val) (RequestMatcher, error)
437 +
438 +// CELMatcherWithErrorFactory converts a constant CEL value into a RequestMatcherWithError.
439 +type CELMatcherWithErrorFactory = func(data ref.Val) (RequestMatcherWithError, error)
440
441 // matcherCELLibrary is a simplistic configurable cel.Library implementation.
442 type matcherCELLibrary struct {
@@ -452,7 +464,7 @@ func (lib *matcherCELLibrary) ProgramOptions() []cel.ProgramOption {
464 // that takes a single argument, and optimizes the implementation to precompile
465 // the matcher and return a function that references the precompiled and
466 // provisioned matcher.
455 -func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.InterpretableDecorator {
467 +func CELMatcherDecorator(funcName string, fac any) interpreter.InterpretableDecorator {
468 return func(i interpreter.Interpretable) (interpreter.Interpretable, error) {
469 call, ok := i.(interpreter.InterpretableCall)
470 if !ok {
@@ -481,35 +493,92 @@ func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.Int
493 // and matcher provisioning should be handled at dynamically.
494 return i, nil
495 }
484 - matcher, err := fac(matcherData.Value())
485 - if err != nil {
486 - return nil, err
496 +
497 + if factory, ok := fac.(CELMatcherWithErrorFactory); ok {
498 + matcher, err := factory(matcherData.Value())
499 + if err != nil {
500 + return nil, err
501 + }
502 + return interpreter.NewCall(
503 + i.ID(), funcName, funcName+"_opt",
504 + []interpreter.Interpretable{reqAttr},
505 + func(args ...ref.Val) ref.Val {
506 + // The request value, guaranteed to be of type celHTTPRequest
507 + celReq := args[0]
508 + // If needed this call could be changed to convert the value
509 + // to a *http.Request using CEL's ConvertToNative method.
510 + httpReq := celReq.Value().(celHTTPRequest)
511 + match, err := matcher.MatchWithError(httpReq.Request)
512 + if err != nil {
513 + return types.WrapErr(err)
514 + }
515 + return types.Bool(match)
516 + },
517 + ), nil
518 }
488 - return interpreter.NewCall(
489 - i.ID(), funcName, funcName+"_opt",
490 - []interpreter.Interpretable{reqAttr},
491 - func(args ...ref.Val) ref.Val {
492 - // The request value, guaranteed to be of type celHTTPRequest
493 - celReq := args[0]
494 - // If needed this call could be changed to convert the value
495 - // to a *http.Request using CEL's ConvertToNative method.
496 - httpReq := celReq.Value().(celHTTPRequest)
497 - return types.Bool(matcher.Match(httpReq.Request))
498 - },
499 - ), nil
519 +
520 + if factory, ok := fac.(CELMatcherFactory); ok {
521 + matcher, err := factory(matcherData.Value())
522 + if err != nil {
523 + return nil, err
524 + }
525 + return interpreter.NewCall(
526 + i.ID(), funcName, funcName+"_opt",
527 + []interpreter.Interpretable{reqAttr},
528 + func(args ...ref.Val) ref.Val {
529 + // The request value, guaranteed to be of type celHTTPRequest
530 + celReq := args[0]
531 + // If needed this call could be changed to convert the value
532 + // to a *http.Request using CEL's ConvertToNative method.
533 + httpReq := celReq.Value().(celHTTPRequest)
534 + if m, ok := matcher.(RequestMatcherWithError); ok {
535 + match, err := m.MatchWithError(httpReq.Request)
536 + if err != nil {
537 + return types.WrapErr(err)
538 + }
539 + return types.Bool(match)
540 + }
541 + return types.Bool(matcher.Match(httpReq.Request))
542 + },
543 + ), nil
544 + }
545 +
546 + return nil, fmt.Errorf("invalid matcher factory, must be CELMatcherFactory or CELMatcherWithErrorFactory: %T", fac)
547 }
548 }
549
550 // CELMatcherRuntimeFunction creates a function binding for when the input to the matcher
551 // is dynamically resolved rather than a set of static constant values.
505 -func CELMatcherRuntimeFunction(funcName string, fac CELMatcherFactory) functions.BinaryOp {
552 +func CELMatcherRuntimeFunction(funcName string, fac any) functions.BinaryOp {
553 return func(celReq, matcherData ref.Val) ref.Val {
507 - matcher, err := fac(matcherData)
508 - if err != nil {
509 - return types.WrapErr(err)
554 + if factory, ok := fac.(CELMatcherWithErrorFactory); ok {
555 + matcher, err := factory(matcherData)
556 + if err != nil {
557 + return types.WrapErr(err)
558 + }
559 + httpReq := celReq.Value().(celHTTPRequest)
560 + match, err := matcher.MatchWithError(httpReq.Request)
561 + if err != nil {
562 + return types.WrapErr(err)
563 + }
564 + return types.Bool(match)
565 + }
566 + if factory, ok := fac.(CELMatcherFactory); ok {
567 + matcher, err := factory(matcherData)
568 + if err != nil {
569 + return types.WrapErr(err)
570 + }
571 + httpReq := celReq.Value().(celHTTPRequest)
572 + if m, ok := matcher.(RequestMatcherWithError); ok {
573 + match, err := m.MatchWithError(httpReq.Request)
574 + if err != nil {
575 + return types.WrapErr(err)
576 + }
577 + return types.Bool(match)
578 + }
579 + return types.Bool(matcher.Match(httpReq.Request))
580 }
511 - httpReq := celReq.Value().(celHTTPRequest)
512 - return types.Bool(matcher.Match(httpReq.Request))
581 + return types.NewErr("CELMatcherRuntimeFunction invalid matcher factory: %T", fac)
582 }
583 }
584
@@ -733,9 +802,9 @@ const MatcherNameCtxKey = "matcher_name"
802
803 // Interface guards
804 var (
736 - _ caddy.Provisioner = (*MatchExpression)(nil)
737 - _ RequestMatcher = (*MatchExpression)(nil)
738 - _ caddyfile.Unmarshaler = (*MatchExpression)(nil)
739 - _ json.Marshaler = (*MatchExpression)(nil)
740 - _ json.Unmarshaler = (*MatchExpression)(nil)
805 + _ caddy.Provisioner = (*MatchExpression)(nil)
806 + _ RequestMatcherWithError = (*MatchExpression)(nil)
807 + _ caddyfile.Unmarshaler = (*MatchExpression)(nil)
808 + _ json.Marshaler = (*MatchExpression)(nil)
809 + _ json.Unmarshaler = (*MatchExpression)(nil)
810 )
modules/caddyhttp/celmatcher_test.go
+6 -2
@@ -489,7 +489,11 @@ func TestMatchExpressionMatch(t *testing.T) {
489 }
490 }
491
492 - if tc.expression.Match(req) != tc.wantResult {
492 + matches, err := tc.expression.MatchWithError(req)
493 + if err != nil {
494 + t.Errorf("MatchExpression.Match() error = %v", err)
495 + }
496 + if matches != tc.wantResult {
497 t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
498 }
499 })
@@ -532,7 +536,7 @@ func BenchmarkMatchExpressionMatch(b *testing.B) {
536 }
537 b.ResetTimer()
538 for i := 0; i < b.N; i++ {
535 - tc.expression.Match(req)
539 + tc.expression.MatchWithError(req)
540 }
541 })
542 }
modules/caddyhttp/fileserver/matcher.go
+29 -16
@@ -173,7 +173,7 @@ func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
173 func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
174 requestType := cel.ObjectType("http.Request")
175
176 - matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcher, error) {
176 + matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcherWithError, error) {
177 values, err := caddyhttp.CELValueToMapStrList(data)
178 if err != nil {
179 return nil, err
@@ -313,12 +313,22 @@ func (m MatchFile) Validate() error {
313 // - http.matchers.file.type: file or directory
314 // - http.matchers.file.remainder: Portion remaining after splitting file path (if configured)
315 func (m MatchFile) Match(r *http.Request) bool {
316 + match, err := m.selectFile(r)
317 + if err != nil {
318 + // nolint:staticcheck
319 + caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
320 + }
321 + return match
322 +}
323 +
324 +// MatchWithError returns true if r matches m.
325 +func (m MatchFile) MatchWithError(r *http.Request) (bool, error) {
326 return m.selectFile(r)
327 }
328
329 // selectFile chooses a file according to m.TryPolicy by appending
330 // the paths in m.TryFiles to m.Root, with placeholder replacements.
321 -func (m MatchFile) selectFile(r *http.Request) (matched bool) {
331 +func (m MatchFile) selectFile(r *http.Request) (bool, error) {
332 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
333
334 root := filepath.Clean(repl.ReplaceAll(m.Root, "."))
@@ -330,7 +340,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
340 if c := m.logger.Check(zapcore.ErrorLevel, "use of unregistered filesystem"); c != nil {
341 c.Write(zap.String("fs", fsName))
342 }
333 - return false
343 + return false, nil
344 }
345 type matchCandidate struct {
346 fullpath, relative, splitRemainder string
@@ -421,15 +431,18 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
431 switch m.TryPolicy {
432 case "", tryPolicyFirstExist:
433 for _, pattern := range m.TryFiles {
434 + // If the pattern is a status code, emit an error,
435 + // which short-circuits the middleware pipeline and
436 + // writes an HTTP error response.
437 if err := parseErrorCode(pattern); err != nil {
425 - caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
426 - return
438 + return false, err
439 }
440 +
441 candidates := makeCandidates(pattern)
442 for _, c := range candidates {
443 if info, exists := m.strictFileExists(fileSystem, c.fullpath); exists {
444 setPlaceholders(c, info)
432 - return true
445 + return true, nil
446 }
447 }
448 }
@@ -450,10 +463,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
463 }
464 }
465 if largestInfo == nil {
453 - return false
466 + return false, nil
467 }
468 setPlaceholders(largest, largestInfo)
456 - return true
469 + return true, nil
470
471 case tryPolicySmallestSize:
472 var smallestSize int64
@@ -471,10 +484,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
484 }
485 }
486 if smallestInfo == nil {
474 - return false
487 + return false, nil
488 }
489 setPlaceholders(smallest, smallestInfo)
477 - return true
490 + return true, nil
491
492 case tryPolicyMostRecentlyMod:
493 var recent matchCandidate
@@ -491,13 +504,13 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
504 }
505 }
506 if recentInfo == nil {
494 - return false
507 + return false, nil
508 }
509 setPlaceholders(recent, recentInfo)
497 - return true
510 + return true, nil
511 }
512
500 - return
513 + return false, nil
514 }
515
516 // parseErrorCode checks if the input is a status
@@ -703,7 +716,7 @@ const (
716
717 // Interface guards
718 var (
706 - _ caddy.Validator = (*MatchFile)(nil)
707 - _ caddyhttp.RequestMatcher = (*MatchFile)(nil)
708 - _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil)
719 + _ caddy.Validator = (*MatchFile)(nil)
720 + _ caddyhttp.RequestMatcherWithError = (*MatchFile)(nil)
721 + _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil)
722 )
modules/caddyhttp/fileserver/matcher_test.go
+14 -3
@@ -130,7 +130,10 @@ func TestFileMatcher(t *testing.T) {
130 req := &http.Request{URL: u}
131 repl := caddyhttp.NewTestReplacer(req)
132
133 - result := m.Match(req)
133 + result, err := m.MatchWithError(req)
134 + if err != nil {
135 + t.Errorf("Test %d: unexpected error: %v", i, err)
136 + }
137 if result != tc.matched {
138 t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
139 }
@@ -240,7 +243,10 @@ func TestPHPFileMatcher(t *testing.T) {
243 req := &http.Request{URL: u}
244 repl := caddyhttp.NewTestReplacer(req)
245
243 - result := m.Match(req)
246 + result, err := m.MatchWithError(req)
247 + if err != nil {
248 + t.Errorf("Test %d: unexpected error: %v", i, err)
249 + }
250 if result != tc.matched {
251 t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
252 }
@@ -389,7 +395,12 @@ func TestMatchExpressionMatch(t *testing.T) {
395 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
396 req = req.WithContext(ctx)
397
392 - if tc.expression.Match(req) != tc.wantResult {
398 + matches, err := tc.expression.MatchWithError(req)
399 + if err != nil {
400 + t.Errorf("MatchExpression.Match() error = %v", err)
401 + return
402 + }
403 + if matches != tc.wantResult {
404 t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
405 }
406
modules/caddyhttp/ip_matchers.go
+45 -17
@@ -108,7 +108,7 @@ func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
108 // internal data type of the MatchPath value.
109 []*cel.Type{cel.ListType(cel.StringType)},
110 // function to convert a constant list of strings to a MatchPath instance.
111 - func(data ref.Val) (RequestMatcher, error) {
111 + func(data ref.Val) (RequestMatcherWithError, error) {
112 refStringList := reflect.TypeOf([]string{})
113 strList, err := data.ConvertToNative(refStringList)
114 if err != nil {
@@ -145,9 +145,23 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
145
146 // Match returns true if r matches m.
147 func (m MatchRemoteIP) Match(r *http.Request) bool {
148 + match, err := m.MatchWithError(r)
149 + if err != nil {
150 + SetVar(r.Context(), MatcherErrorVarKey, err)
151 + }
152 + return match
153 +}
154 +
155 +// MatchWithError returns true if r matches m.
156 +func (m MatchRemoteIP) MatchWithError(r *http.Request) (bool, error) {
157 + // if handshake is not finished, we infer 0-RTT that has
158 + // not verified remote IP; could be spoofed, so we throw
159 + // HTTP 425 status to tell the client to try again after
160 + // the handshake is complete
161 if r.TLS != nil && !r.TLS.HandshakeComplete {
149 - return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed
162 + return false, Error(http.StatusTooEarly, fmt.Errorf("TLS handshake not complete, remote IP cannot be verified"))
163 }
164 +
165 address := r.RemoteAddr
166 clientIP, zoneID, err := parseIPZoneFromString(address)
167 if err != nil {
@@ -155,7 +169,7 @@ func (m MatchRemoteIP) Match(r *http.Request) bool {
169 c.Write(zap.Error(err))
170 }
171
158 - return false
172 + return false, nil
173 }
174 matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones)
175 if !matches && !zoneFilter {
@@ -163,7 +177,7 @@ func (m MatchRemoteIP) Match(r *http.Request) bool {
177 c.Write(zap.String("zone", zoneID))
178 }
179 }
166 - return matches
180 + return matches, nil
181 }
182
183 // CaddyModule returns the Caddy module information.
@@ -207,7 +221,7 @@ func (MatchClientIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
221 // internal data type of the MatchPath value.
222 []*cel.Type{cel.ListType(cel.StringType)},
223 // function to convert a constant list of strings to a MatchPath instance.
210 - func(data ref.Val) (RequestMatcher, error) {
224 + func(data ref.Val) (RequestMatcherWithError, error) {
225 refStringList := reflect.TypeOf([]string{})
226 strList, err := data.ConvertToNative(refStringList)
227 if err != nil {
@@ -238,20 +252,34 @@ func (m *MatchClientIP) Provision(ctx caddy.Context) error {
252
253 // Match returns true if r matches m.
254 func (m MatchClientIP) Match(r *http.Request) bool {
255 + match, err := m.MatchWithError(r)
256 + if err != nil {
257 + SetVar(r.Context(), MatcherErrorVarKey, err)
258 + }
259 + return match
260 +}
261 +
262 +// MatchWithError returns true if r matches m.
263 +func (m MatchClientIP) MatchWithError(r *http.Request) (bool, error) {
264 + // if handshake is not finished, we infer 0-RTT that has
265 + // not verified remote IP; could be spoofed, so we throw
266 + // HTTP 425 status to tell the client to try again after
267 + // the handshake is complete
268 if r.TLS != nil && !r.TLS.HandshakeComplete {
242 - return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed
269 + return false, Error(http.StatusTooEarly, fmt.Errorf("TLS handshake not complete, remote IP cannot be verified"))
270 }
271 +
272 address := GetVar(r.Context(), ClientIPVarKey).(string)
273 clientIP, zoneID, err := parseIPZoneFromString(address)
274 if err != nil {
275 m.logger.Error("getting client IP", zap.Error(err))
248 - return false
276 + return false, nil
277 }
278 matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones)
279 if !matches && !zoneFilter {
280 m.logger.Debug("zone ID from client IP did not match", zap.String("zone", zoneID))
281 }
254 - return matches
282 + return matches, nil
283 }
284
285 func provisionCidrsZonesFromRanges(ranges []string) ([]*netip.Prefix, []string, error) {
@@ -326,13 +354,13 @@ func matchIPByCidrZones(clientIP netip.Addr, zoneID string, cidrs []*netip.Prefi
354
355 // Interface guards
356 var (
329 - _ RequestMatcher = (*MatchRemoteIP)(nil)
330 - _ caddy.Provisioner = (*MatchRemoteIP)(nil)
331 - _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
332 - _ CELLibraryProducer = (*MatchRemoteIP)(nil)
333 -
334 - _ RequestMatcher = (*MatchClientIP)(nil)
335 - _ caddy.Provisioner = (*MatchClientIP)(nil)
336 - _ caddyfile.Unmarshaler = (*MatchClientIP)(nil)
337 - _ CELLibraryProducer = (*MatchClientIP)(nil)
357 + _ RequestMatcherWithError = (*MatchRemoteIP)(nil)
358 + _ caddy.Provisioner = (*MatchRemoteIP)(nil)
359 + _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
360 + _ CELLibraryProducer = (*MatchRemoteIP)(nil)
361 +
362 + _ RequestMatcherWithError = (*MatchClientIP)(nil)
363 + _ caddy.Provisioner = (*MatchClientIP)(nil)
364 + _ caddyfile.Unmarshaler = (*MatchClientIP)(nil)
365 + _ CELLibraryProducer = (*MatchClientIP)(nil)
366 )
modules/caddyhttp/matchers.go
+151 -68
@@ -296,6 +296,12 @@ func (m MatchHost) Provision(_ caddy.Context) error {
296
297 // Match returns true if r matches m.
298 func (m MatchHost) Match(r *http.Request) bool {
299 + match, _ := m.MatchWithError(r)
300 + return match
301 +}
302 +
303 +// MatchWithError returns true if r matches m.
304 +func (m MatchHost) MatchWithError(r *http.Request) (bool, error) {
305 reqHost, _, err := net.SplitHostPort(r.Host)
306 if err != nil {
307 // OK; probably didn't have a port
@@ -315,7 +321,7 @@ func (m MatchHost) Match(r *http.Request) bool {
321 return m[i] >= reqHost
322 })
323 if pos < len(m) && m[pos] == reqHost {
318 - return true
324 + return true, nil
325 }
326 }
327
@@ -346,13 +352,13 @@ outer:
352 continue outer
353 }
354 }
349 - return true
355 + return true, nil
356 } else if strings.EqualFold(reqHost, host) {
351 - return true
357 + return true, nil
358 }
359 }
360
355 - return false
361 + return false, nil
362 }
363
364 // CELLibrary produces options that expose this matcher for use in CEL
@@ -366,7 +372,7 @@ func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) {
372 "host",
373 "host_match_request_list",
374 []*cel.Type{cel.ListType(cel.StringType)},
369 - func(data ref.Val) (RequestMatcher, error) {
375 + func(data ref.Val) (RequestMatcherWithError, error) {
376 refStringList := reflect.TypeOf([]string{})
377 strList, err := data.ConvertToNative(refStringList)
378 if err != nil {
@@ -411,6 +417,12 @@ func (m MatchPath) Provision(_ caddy.Context) error {
417
418 // Match returns true if r matches m.
419 func (m MatchPath) Match(r *http.Request) bool {
420 + match, _ := m.MatchWithError(r)
421 + return match
422 +}
423 +
424 +// MatchWithError returns true if r matches m.
425 +func (m MatchPath) MatchWithError(r *http.Request) (bool, error) {
426 // Even though RFC 9110 says that path matching is case-sensitive
427 // (https://www.rfc-editor.org/rfc/rfc9110.html#section-4.2.3),
428 // we do case-insensitive matching to mitigate security issues
@@ -436,7 +448,7 @@ func (m MatchPath) Match(r *http.Request) bool {
448 // special case: whole path is wildcard; this is unnecessary
449 // as it matches all requests, which is the same as no matcher
450 if matchPattern == "*" {
439 - return true
451 + return true, nil
452 }
453
454 // Clean the path, merge doubled slashes, etc.
@@ -464,7 +476,7 @@ func (m MatchPath) Match(r *http.Request) bool {
476 if strings.Contains(matchPattern, "%") {
477 reqPathForPattern := CleanPath(r.URL.EscapedPath(), mergeSlashes)
478 if m.matchPatternWithEscapeSequence(reqPathForPattern, matchPattern) {
467 - return true
479 + return true, nil
480 }
481
482 // doing prefix/suffix/substring matches doesn't make sense
@@ -483,7 +495,7 @@ func (m MatchPath) Match(r *http.Request) bool {
495 strings.HasPrefix(matchPattern, "*") &&
496 strings.HasSuffix(matchPattern, "*") {
497 if strings.Contains(reqPathForPattern, matchPattern[1:len(matchPattern)-1]) {
486 - return true
498 + return true, nil
499 }
500 continue
501 }
@@ -495,7 +507,7 @@ func (m MatchPath) Match(r *http.Request) bool {
507 // treat it as a fast suffix match
508 if strings.HasPrefix(matchPattern, "*") {
509 if strings.HasSuffix(reqPathForPattern, matchPattern[1:]) {
498 - return true
510 + return true, nil
511 }
512 continue
513 }
@@ -504,7 +516,7 @@ func (m MatchPath) Match(r *http.Request) bool {
516 // treat it as a fast prefix match
517 if strings.HasSuffix(matchPattern, "*") {
518 if strings.HasPrefix(reqPathForPattern, matchPattern[:len(matchPattern)-1]) {
507 - return true
519 + return true, nil
520 }
521 continue
522 }
@@ -515,10 +527,10 @@ func (m MatchPath) Match(r *http.Request) bool {
527 // because we can't handle it anyway
528 matches, _ := path.Match(matchPattern, reqPathForPattern)
529 if matches {
518 - return true
530 + return true, nil
531 }
532 }
521 - return false
533 + return false, nil
534 }
535
536 func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool {
@@ -642,7 +654,7 @@ func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) {
654 // internal data type of the MatchPath value.
655 []*cel.Type{cel.ListType(cel.StringType)},
656 // function to convert a constant list of strings to a MatchPath instance.
645 - func(data ref.Val) (RequestMatcher, error) {
657 + func(data ref.Val) (RequestMatcherWithError, error) {
658 refStringList := reflect.TypeOf([]string{})
659 strList, err := data.ConvertToNative(refStringList)
660 if err != nil {
@@ -677,6 +689,12 @@ func (MatchPathRE) CaddyModule() caddy.ModuleInfo {
689
690 // Match returns true if r matches m.
691 func (m MatchPathRE) Match(r *http.Request) bool {
692 + match, _ := m.MatchWithError(r)
693 + return match
694 +}
695 +
696 +// MatchWithError returns true if r matches m.
697 +func (m MatchPathRE) MatchWithError(r *http.Request) (bool, error) {
698 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
699
700 // Clean the path, merges doubled slashes, etc.
@@ -684,7 +702,7 @@ func (m MatchPathRE) Match(r *http.Request) bool {
702 // the path matcher. See #4407
703 cleanedPath := cleanPath(r.URL.Path)
704
687 - return m.MatchRegexp.Match(cleanedPath, repl)
705 + return m.MatchRegexp.Match(cleanedPath, repl), nil
706 }
707
708 // CELLibrary produces options that expose this matcher for use in CEL
@@ -698,7 +716,7 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
716 "path_regexp",
717 "path_regexp_request_string",
718 []*cel.Type{cel.StringType},
701 - func(data ref.Val) (RequestMatcher, error) {
719 + func(data ref.Val) (RequestMatcherWithError, error) {
720 pattern := data.(types.String)
721 matcher := MatchPathRE{MatchRegexp{
722 Name: ctx.Value(MatcherNameCtxKey).(string),
@@ -715,7 +733,7 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
733 "path_regexp",
734 "path_regexp_request_string_string",
735 []*cel.Type{cel.StringType, cel.StringType},
718 - func(data ref.Val) (RequestMatcher, error) {
736 + func(data ref.Val) (RequestMatcherWithError, error) {
737 refStringList := reflect.TypeOf([]string{})
738 params, err := data.ConvertToNative(refStringList)
739 if err != nil {
@@ -764,7 +782,13 @@ func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
782
783 // Match returns true if r matches m.
784 func (m MatchMethod) Match(r *http.Request) bool {
767 - return slices.Contains(m, r.Method)
785 + match, _ := m.MatchWithError(r)
786 + return match
787 +}
788 +
789 +// MatchWithError returns true if r matches m.
790 +func (m MatchMethod) MatchWithError(r *http.Request) (bool, error) {
791 + return slices.Contains(m, r.Method), nil
792 }
793
794 // CELLibrary produces options that expose this matcher for use in CEL
@@ -778,7 +802,7 @@ func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) {
802 "method",
803 "method_request_list",
804 []*cel.Type{cel.ListType(cel.StringType)},
781 - func(data ref.Val) (RequestMatcher, error) {
805 + func(data ref.Val) (RequestMatcherWithError, error) {
806 refStringList := reflect.TypeOf([]string{})
807 strList, err := data.ConvertToNative(refStringList)
808 if err != nil {
@@ -823,10 +847,17 @@ func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
847
848 // Match returns true if r matches m. An empty m matches an empty query string.
849 func (m MatchQuery) Match(r *http.Request) bool {
850 + match, _ := m.MatchWithError(r)
851 + return match
852 +}
853 +
854 +// MatchWithError returns true if r matches m.
855 +// An empty m matches an empty query string.
856 +func (m MatchQuery) MatchWithError(r *http.Request) (bool, error) {
857 // If no query keys are configured, this only
858 // matches an empty query string.
859 if len(m) == 0 {
829 - return len(r.URL.Query()) == 0
860 + return len(r.URL.Query()) == 0, nil
861 }
862
863 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@@ -843,7 +874,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
874 // "Relying on parser alignment for security is doomed." Overall conclusion is that
875 // splitting on & and rejecting ; in key=value pairs is safer than accepting raw ;.
876 // We regard the Go team's decision as sound and thus reject malformed query strings.
846 - return false
877 + return false, nil
878 }
879
880 // Count the amount of matched keys, to ensure we AND
@@ -854,7 +885,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
885 param = repl.ReplaceAll(param, "")
886 paramVal, found := parsed[param]
887 if !found {
857 - return false
888 + return false, nil
889 }
890 for _, v := range vals {
891 v = repl.ReplaceAll(v, "")
@@ -864,7 +895,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
895 }
896 }
897 }
867 - return matchedKeys == len(m)
898 + return matchedKeys == len(m), nil
899 }
900
901 // CELLibrary produces options that expose this matcher for use in CEL
@@ -878,7 +909,7 @@ func (MatchQuery) CELLibrary(_ caddy.Context) (cel.Library, error) {
909 "query",
910 "query_matcher_request_map",
911 []*cel.Type{CELTypeJSON},
881 - func(data ref.Val) (RequestMatcher, error) {
912 + func(data ref.Val) (RequestMatcherWithError, error) {
913 mapStrListStr, err := CELValueToMapStrList(data)
914 if err != nil {
915 return nil, err
@@ -940,8 +971,14 @@ func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
971
972 // Match returns true if r matches m.
973 func (m MatchHeader) Match(r *http.Request) bool {
974 + match, _ := m.MatchWithError(r)
975 + return match
976 +}
977 +
978 +// MatchWithError returns true if r matches m.
979 +func (m MatchHeader) MatchWithError(r *http.Request) (bool, error) {
980 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
944 - return matchHeaders(r.Header, http.Header(m), r.Host, repl)
981 + return matchHeaders(r.Header, http.Header(m), r.Host, repl), nil
982 }
983
984 // CELLibrary produces options that expose this matcher for use in CEL
@@ -956,7 +993,7 @@ func (MatchHeader) CELLibrary(_ caddy.Context) (cel.Library, error) {
993 "header",
994 "header_matcher_request_map",
995 []*cel.Type{CELTypeJSON},
959 - func(data ref.Val) (RequestMatcher, error) {
996 + func(data ref.Val) (RequestMatcherWithError, error) {
997 mapStrListStr, err := CELValueToMapStrList(data)
998 if err != nil {
999 return nil, err
@@ -1075,6 +1112,12 @@ func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1112
1113 // Match returns true if r matches m.
1114 func (m MatchHeaderRE) Match(r *http.Request) bool {
1115 + match, _ := m.MatchWithError(r)
1116 + return match
1117 +}
1118 +
1119 +// MatchWithError returns true if r matches m.
1120 +func (m MatchHeaderRE) MatchWithError(r *http.Request) (bool, error) {
1121 for field, rm := range m {
1122 actualFieldVals := getHeaderFieldVals(r.Header, field, r.Host)
1123 match := false
@@ -1087,10 +1130,10 @@ func (m MatchHeaderRE) Match(r *http.Request) bool {
1130 }
1131 }
1132 if !match {
1090 - return false
1133 + return false, nil
1134 }
1135 }
1093 - return true
1136 + return true, nil
1137 }
1138
1139 // Provision compiles m's regular expressions.
@@ -1126,7 +1169,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
1169 "header_regexp",
1170 "header_regexp_request_string_string",
1171 []*cel.Type{cel.StringType, cel.StringType},
1129 - func(data ref.Val) (RequestMatcher, error) {
1172 + func(data ref.Val) (RequestMatcherWithError, error) {
1173 refStringList := reflect.TypeOf([]string{})
1174 params, err := data.ConvertToNative(refStringList)
1175 if err != nil {
@@ -1149,7 +1192,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
1192 "header_regexp",
1193 "header_regexp_request_string_string_string",
1194 []*cel.Type{cel.StringType, cel.StringType, cel.StringType},
1152 - func(data ref.Val) (RequestMatcher, error) {
1195 + func(data ref.Val) (RequestMatcherWithError, error) {
1196 refStringList := reflect.TypeOf([]string{})
1197 params, err := data.ConvertToNative(refStringList)
1198 if err != nil {
@@ -1187,31 +1230,37 @@ func (MatchProtocol) CaddyModule() caddy.ModuleInfo {
1230
1231 // Match returns true if r matches m.
1232 func (m MatchProtocol) Match(r *http.Request) bool {
1233 + match, _ := m.MatchWithError(r)
1234 + return match
1235 +}
1236 +
1237 +// MatchWithError returns true if r matches m.
1238 +func (m MatchProtocol) MatchWithError(r *http.Request) (bool, error) {
1239 switch string(m) {
1240 case "grpc":
1192 - return strings.HasPrefix(r.Header.Get("content-type"), "application/grpc")
1241 + return strings.HasPrefix(r.Header.Get("content-type"), "application/grpc"), nil
1242 case "https":
1194 - return r.TLS != nil
1243 + return r.TLS != nil, nil
1244 case "http":
1196 - return r.TLS == nil
1245 + return r.TLS == nil, nil
1246 case "http/1.0":
1198 - return r.ProtoMajor == 1 && r.ProtoMinor == 0
1247 + return r.ProtoMajor == 1 && r.ProtoMinor == 0, nil
1248 case "http/1.0+":
1200 - return r.ProtoAtLeast(1, 0)
1249 + return r.ProtoAtLeast(1, 0), nil
1250 case "http/1.1":
1202 - return r.ProtoMajor == 1 && r.ProtoMinor == 1
1251 + return r.ProtoMajor == 1 && r.ProtoMinor == 1, nil
1252 case "http/1.1+":
1204 - return r.ProtoAtLeast(1, 1)
1253 + return r.ProtoAtLeast(1, 1), nil
1254 case "http/2":
1206 - return r.ProtoMajor == 2
1255 + return r.ProtoMajor == 2, nil
1256 case "http/2+":
1208 - return r.ProtoAtLeast(2, 0)
1257 + return r.ProtoAtLeast(2, 0), nil
1258 case "http/3":
1210 - return r.ProtoMajor == 3
1259 + return r.ProtoMajor == 3, nil
1260 case "http/3+":
1212 - return r.ProtoAtLeast(3, 0)
1261 + return r.ProtoAtLeast(3, 0), nil
1262 }
1214 - return false
1263 + return false, nil
1264 }
1265
1266 // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
@@ -1238,7 +1287,7 @@ func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) {
1287 "protocol",
1288 "protocol_request_string",
1289 []*cel.Type{cel.StringType},
1241 - func(data ref.Val) (RequestMatcher, error) {
1290 + func(data ref.Val) (RequestMatcherWithError, error) {
1291 protocolStr, ok := data.(types.String)
1292 if !ok {
1293 return nil, errors.New("protocol argument was not a string")
@@ -1258,16 +1307,22 @@ func (MatchTLS) CaddyModule() caddy.ModuleInfo {
1307
1308 // Match returns true if r matches m.
1309 func (m MatchTLS) Match(r *http.Request) bool {
1310 + match, _ := m.MatchWithError(r)
1311 + return match
1312 +}
1313 +
1314 +// MatchWithError returns true if r matches m.
1315 +func (m MatchTLS) MatchWithError(r *http.Request) (bool, error) {
1316 if r.TLS == nil {
1262 - return false
1317 + return false, nil
1318 }
1319 if m.HandshakeComplete != nil {
1320 if (!*m.HandshakeComplete && r.TLS.HandshakeComplete) ||
1321 (*m.HandshakeComplete && !r.TLS.HandshakeComplete) {
1267 - return false
1322 + return false, nil
1323 }
1324 }
1270 - return true
1325 + return true, nil
1326 }
1327
1328 // UnmarshalCaddyfile parses Caddyfile tokens for this matcher. Syntax:
@@ -1337,7 +1392,15 @@ func (m *MatchNot) Provision(ctx caddy.Context) error {
1392 for _, modMap := range matcherSets.([]map[string]any) {
1393 var ms MatcherSet
1394 for _, modIface := range modMap {
1340 - ms = append(ms, modIface.(RequestMatcher))
1395 + if mod, ok := modIface.(RequestMatcherWithError); ok {
1396 + ms = append(ms, mod)
1397 + continue
1398 + }
1399 + if mod, ok := modIface.(RequestMatcher); ok {
1400 + ms = append(ms, mod)
1401 + continue
1402 + }
1403 + return fmt.Errorf("module is not a request matcher: %T", modIface)
1404 }
1405 m.MatcherSets = append(m.MatcherSets, ms)
1406 }
@@ -1348,12 +1411,24 @@ func (m *MatchNot) Provision(ctx caddy.Context) error {
1411 // the embedded matchers, false is returned if any of its matcher
1412 // sets return true.
1413 func (m MatchNot) Match(r *http.Request) bool {
1414 + match, _ := m.MatchWithError(r)
1415 + return match
1416 +}
1417 +
1418 +// MatchWithError returns true if r matches m. Since this matcher
1419 +// negates the embedded matchers, false is returned if any of its
1420 +// matcher sets return true.
1421 +func (m MatchNot) MatchWithError(r *http.Request) (bool, error) {
1422 for _, ms := range m.MatcherSets {
1352 - if ms.Match(r) {
1353 - return false
1423 + matches, err := ms.MatchWithError(r)
1424 + if err != nil {
1425 + return false, err
1426 + }
1427 + if matches {
1428 + return false, nil
1429 }
1430 }
1356 - return true
1431 + return true, nil
1432 }
1433
1434 // MatchRegexp is an embedable type for matching
@@ -1469,7 +1544,7 @@ func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1544 // ParseCaddyfileNestedMatcher parses the Caddyfile tokens for a nested
1545 // matcher set, and returns its raw module map value.
1546 func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) {
1472 - matcherMap := make(map[string]RequestMatcher)
1547 + matcherMap := make(map[string]any)
1548
1549 // in case there are multiple instances of the same matcher, concatenate
1550 // their tokens (we expect that UnmarshalCaddyfile should be able to
@@ -1494,11 +1569,15 @@ func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, er
1569 if err != nil {
1570 return nil, err
1571 }
1497 - rm, ok := unm.(RequestMatcher)
1498 - if !ok {
1499 - return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
1572 + if rm, ok := unm.(RequestMatcherWithError); ok {
1573 + matcherMap[matcherName] = rm
1574 + continue
1575 }
1501 - matcherMap[matcherName] = rm
1576 + if rm, ok := unm.(RequestMatcher); ok {
1577 + matcherMap[matcherName] = rm
1578 + continue
1579 + }
1580 + return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
1581 }
1582
1583 // we should now have a functional matcher, but we also
@@ -1524,24 +1603,28 @@ const regexpPlaceholderPrefix = "http.regexp"
1603 // holds an optional error emitted from a request matcher,
1604 // to short-circuit the handler chain, since matchers cannot
1605 // return errors via the RequestMatcher interface.
1606 +//
1607 +// Deprecated: Matchers should implement RequestMatcherWithError
1608 +// which can return an error directly, instead of smuggling it
1609 +// through the vars map.
1610 const MatcherErrorVarKey = "matchers.error"
1611
1612 // Interface guards
1613 var (
1531 - _ RequestMatcher = (*MatchHost)(nil)
1532 - _ caddy.Provisioner = (*MatchHost)(nil)
1533 - _ RequestMatcher = (*MatchPath)(nil)
1534 - _ RequestMatcher = (*MatchPathRE)(nil)
1535 - _ caddy.Provisioner = (*MatchPathRE)(nil)
1536 - _ RequestMatcher = (*MatchMethod)(nil)
1537 - _ RequestMatcher = (*MatchQuery)(nil)
1538 - _ RequestMatcher = (*MatchHeader)(nil)
1539 - _ RequestMatcher = (*MatchHeaderRE)(nil)
1540 - _ caddy.Provisioner = (*MatchHeaderRE)(nil)
1541 - _ RequestMatcher = (*MatchProtocol)(nil)
1542 - _ RequestMatcher = (*MatchNot)(nil)
1543 - _ caddy.Provisioner = (*MatchNot)(nil)
1544 - _ caddy.Provisioner = (*MatchRegexp)(nil)
1614 + _ RequestMatcherWithError = (*MatchHost)(nil)
1615 + _ caddy.Provisioner = (*MatchHost)(nil)
1616 + _ RequestMatcherWithError = (*MatchPath)(nil)
1617 + _ RequestMatcherWithError = (*MatchPathRE)(nil)
1618 + _ caddy.Provisioner = (*MatchPathRE)(nil)
1619 + _ RequestMatcherWithError = (*MatchMethod)(nil)
1620 + _ RequestMatcherWithError = (*MatchQuery)(nil)
1621 + _ RequestMatcherWithError = (*MatchHeader)(nil)
1622 + _ RequestMatcherWithError = (*MatchHeaderRE)(nil)
1623 + _ caddy.Provisioner = (*MatchHeaderRE)(nil)
1624 + _ RequestMatcherWithError = (*MatchProtocol)(nil)
1625 + _ RequestMatcherWithError = (*MatchNot)(nil)
1626 + _ caddy.Provisioner = (*MatchNot)(nil)
1627 + _ caddy.Provisioner = (*MatchRegexp)(nil)
1628
1629 _ caddyfile.Unmarshaler = (*MatchHost)(nil)
1630 _ caddyfile.Unmarshaler = (*MatchPath)(nil)
modules/caddyhttp/matchers_test.go
+40 -13
@@ -158,7 +158,10 @@ func TestHostMatcher(t *testing.T) {
158 t.Errorf("Test %d %v: provisioning failed: %v", i, tc.match, err)
159 }
160
161 - actual := tc.match.Match(req)
161 + actual, err := tc.match.MatchWithError(req)
162 + if err != nil {
163 + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
164 + }
165 if actual != tc.expect {
166 t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
167 continue
@@ -430,7 +433,10 @@ func TestPathMatcher(t *testing.T) {
433 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
434 req = req.WithContext(ctx)
435
433 - actual := tc.match.Match(req)
436 + actual, err := tc.match.MatchWithError(req)
437 + if err != nil {
438 + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
439 + }
440 if actual != tc.expect {
441 t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
442 continue
@@ -451,7 +457,10 @@ func TestPathMatcherWindows(t *testing.T) {
457 req = req.WithContext(ctx)
458
459 match := MatchPath{"*.php"}
454 - matched := match.Match(req)
460 + matched, err := match.MatchWithError(req)
461 + if err != nil {
462 + t.Errorf("Expected no error, but got: %v", err)
463 + }
464 if !matched {
465 t.Errorf("Expected to match; should ignore trailing dots and spaces")
466 }
@@ -555,7 +564,10 @@ func TestPathREMatcher(t *testing.T) {
564 req = req.WithContext(ctx)
565 addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
566
558 - actual := tc.match.Match(req)
567 + actual, err := tc.match.MatchWithError(req)
568 + if err != nil {
569 + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
570 + }
571 if actual != tc.expect {
572 t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
573 i, tc.match.Pattern, tc.expect, actual, tc.input)
@@ -691,7 +703,10 @@ func TestHeaderMatcher(t *testing.T) {
703 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
704 req = req.WithContext(ctx)
705
694 - actual := tc.match.Match(req)
706 + actual, err := tc.match.MatchWithError(req)
707 + if err != nil {
708 + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
709 + }
710 if actual != tc.expect {
711 t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
712 continue
@@ -818,7 +833,10 @@ func TestQueryMatcher(t *testing.T) {
833 repl.Set("http.vars.debug", "1")
834 repl.Set("http.vars.key", "somekey")
835 req = req.WithContext(ctx)
821 - actual := tc.match.Match(req)
836 + actual, err := tc.match.MatchWithError(req)
837 + if err != nil {
838 + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
839 + }
840 if actual != tc.expect {
841 t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
842 continue
@@ -887,7 +905,10 @@ func TestHeaderREMatcher(t *testing.T) {
905 req = req.WithContext(ctx)
906 addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
907
890 - actual := tc.match.Match(req)
908 + actual, err := tc.match.MatchWithError(req)
909 + if err != nil {
910 + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
911 + }
912 if actual != tc.expect {
913 t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
914 i, tc.match, tc.expect, actual, tc.input)
@@ -927,7 +948,7 @@ func BenchmarkHeaderREMatcher(b *testing.B) {
948 req = req.WithContext(ctx)
949 addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
950 for run := 0; run < b.N; run++ {
930 - match.Match(req)
951 + match.MatchWithError(req)
952 }
953 }
954
@@ -998,7 +1019,10 @@ func TestVarREMatcher(t *testing.T) {
1019
1020 tc.input.ServeHTTP(httptest.NewRecorder(), req, emptyHandler)
1021
1001 - actual := tc.match.Match(req)
1022 + actual, err := tc.match.MatchWithError(req)
1023 + if err != nil {
1024 + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
1025 + }
1026 if actual != tc.expect {
1027 t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
1028 i, tc.match, tc.expect, actual, tc.input)
@@ -1123,7 +1147,10 @@ func TestNotMatcher(t *testing.T) {
1147 ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
1148 req = req.WithContext(ctx)
1149
1126 - actual := tc.match.Match(req)
1150 + actual, err := tc.match.MatchWithError(req)
1151 + if err != nil {
1152 + t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
1153 + }
1154 if actual != tc.expect {
1155 t.Errorf("Test %d %+v: Expected %t, got %t for: host=%s path=%s'", i, tc.match, tc.expect, actual, tc.host, tc.path)
1156 continue
@@ -1155,7 +1182,7 @@ func BenchmarkLargeHostMatcher(b *testing.B) {
1182
1183 b.ResetTimer()
1184 for i := 0; i < b.N; i++ {
1158 - matcher.Match(req)
1185 + matcher.MatchWithError(req)
1186 }
1187 }
1188
@@ -1169,7 +1196,7 @@ func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) {
1196
1197 b.ResetTimer()
1198 for i := 0; i < b.N; i++ {
1172 - match.Match(req)
1199 + match.MatchWithError(req)
1200 }
1201 }
1202
@@ -1187,6 +1214,6 @@ func BenchmarkHostMatcherWithPlaceholder(b *testing.B) {
1214
1215 b.ResetTimer()
1216 for i := 0; i < b.N; i++ {
1190 - match.Match(req)
1217 + match.MatchWithError(req)
1218 }
1219 }
modules/caddyhttp/reverseproxy/healthchecks.go
+1 -1
@@ -72,7 +72,7 @@ type HealthChecks struct {
72 // health checks (that is, health checks which occur in a
73 // background goroutine independently).
74 type ActiveHealthChecks struct {
75 - // DEPRECATED: Use 'uri' instead. This field will be removed. TODO: remove this field
75 + // Deprecated: Use 'uri' instead. This field will be removed. TODO: remove this field
76 Path string `json:"path,omitempty"`
77
78 // The URI (path and query) to use for health checks
modules/caddyhttp/reverseproxy/httptransport.go
+2 -2
@@ -545,11 +545,11 @@ type TLSConfig struct {
545 // Certificate authority module which provides the certificate pool of trusted certificates
546 CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"`
547
548 - // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.inline` module instead.
548 + // Deprecated: Use the `ca` field with the `tls.ca_pool.source.inline` module instead.
549 // Optional list of base64-encoded DER-encoded CA certificates to trust.
550 RootCAPool []string `json:"root_ca_pool,omitempty"`
551
552 - // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.file` module instead.
552 + // Deprecated: Use the `ca` field with the `tls.ca_pool.source.file` module instead.
553 // List of PEM-encoded CA certificate files to add to the same trust
554 // store as RootCAPool (or root_ca_pool in the JSON).
555 RootCAPEMFiles []string `json:"root_ca_pem_files,omitempty"`
modules/caddyhttp/reverseproxy/reverseproxy.go
+9 -4
@@ -496,7 +496,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
496 if proxyErr == nil {
497 proxyErr = caddyhttp.Error(http.StatusServiceUnavailable, errNoUpstream)
498 }
499 - if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) {
499 + if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) {
500 return true, proxyErr
501 }
502 return false, proxyErr
@@ -562,7 +562,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
562 h.countFailure(upstream)
563
564 // if we've tried long enough, break
565 - if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) {
565 + if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) {
566 return true, proxyErr
567 }
568
@@ -1089,7 +1089,7 @@ func (h *Handler) finalizeResponse(
1089 // If true is returned, it has already blocked long enough before
1090 // the next retry (i.e. no more sleeping is needed). If false is
1091 // returned, the handler should stop trying to proxy the request.
1092 -func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int, proxyErr error, req *http.Request) bool {
1092 +func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int, proxyErr error, req *http.Request, logger *zap.Logger) bool {
1093 // no retries are configured
1094 if lb.TryDuration == 0 && lb.Retries == 0 {
1095 return false
@@ -1124,7 +1124,12 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int
1124 return false
1125 }
1126
1127 - if !lb.RetryMatch.AnyMatch(req) {
1127 + match, err := lb.RetryMatch.AnyMatchWithError(req)
1128 + if err != nil {
1129 + logger.Error("error matching request for retry", zap.Error(err))
1130 + return false
1131 + }
1132 + if !match {
1133 return false
1134 }
1135 }
modules/caddyhttp/routes.go
+78 -19
@@ -254,18 +254,13 @@ func wrapRoute(route Route) Middleware {
254 nextCopy := next
255
256 // route must match at least one of the matcher sets
257 - if !route.MatcherSets.AnyMatch(req) {
257 + matches, err := route.MatcherSets.AnyMatchWithError(req)
258 + if err != nil {
259 // allow matchers the opportunity to short circuit
260 // the request and trigger the error handling chain
260 - err, ok := GetVar(req.Context(), MatcherErrorVarKey).(error)
261 - if ok {
262 - // clear out the error from context, otherwise
263 - // it will cascade to the error routes (#4916)
264 - SetVar(req.Context(), MatcherErrorVarKey, nil)
265 - // return the matcher's error
266 - return err
267 - }
268 -
261 + return err
262 + }
263 + if !matches {
264 // call the next handler, and skip this one,
265 // since the matcher didn't match
266 return nextCopy.ServeHTTP(rw, req)
@@ -341,19 +336,58 @@ func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) M
336 // MatcherSet is a set of matchers which
337 // must all match in order for the request
338 // to be matched successfully.
344 -type MatcherSet []RequestMatcher
339 +type MatcherSet []any
340
341 // Match returns true if the request matches all
342 // matchers in mset or if there are no matchers.
343 func (mset MatcherSet) Match(r *http.Request) bool {
344 for _, m := range mset {
350 - if !m.Match(r) {
351 - return false
345 + if me, ok := m.(RequestMatcherWithError); ok {
346 + match, _ := me.MatchWithError(r)
347 + if !match {
348 + return false
349 + }
350 + continue
351 + }
352 + if me, ok := m.(RequestMatcher); ok {
353 + if !me.Match(r) {
354 + return false
355 + }
356 + continue
357 }
358 + return false
359 }
360 return true
361 }
362
363 +// MatchWithError returns true if r matches m.
364 +func (mset MatcherSet) MatchWithError(r *http.Request) (bool, error) {
365 + for _, m := range mset {
366 + if me, ok := m.(RequestMatcherWithError); ok {
367 + match, err := me.MatchWithError(r)
368 + if err != nil || !match {
369 + return match, err
370 + }
371 + continue
372 + }
373 + if me, ok := m.(RequestMatcher); ok {
374 + if !me.Match(r) {
375 + // for backwards compatibility
376 + err, ok := GetVar(r.Context(), MatcherErrorVarKey).(error)
377 + if ok {
378 + // clear out the error from context since we've consumed it
379 + SetVar(r.Context(), MatcherErrorVarKey, nil)
380 + return false, err
381 + }
382 + return false, nil
383 + }
384 + continue
385 + }
386 + return false, fmt.Errorf("matcher is not a RequestMatcher or RequestMatcherWithError: %#v", m)
387 + }
388 + return true, nil
389 +}
390 +
391 // RawMatcherSets is a group of matcher sets
392 // in their raw, JSON form.
393 type RawMatcherSets []caddy.ModuleMap
@@ -366,25 +400,50 @@ type MatcherSets []MatcherSet
400 // AnyMatch returns true if req matches any of the
401 // matcher sets in ms or if there are no matchers,
402 // in which case the request always matches.
403 +//
404 +// Deprecated: Use AnyMatchWithError instead.
405 func (ms MatcherSets) AnyMatch(req *http.Request) bool {
406 for _, m := range ms {
371 - if m.Match(req) {
372 - return true
407 + match, err := m.MatchWithError(req)
408 + if err != nil {
409 + SetVar(req.Context(), MatcherErrorVarKey, err)
410 + return false
411 + }
412 + if match {
413 + return match
414 }
415 }
416 return len(ms) == 0
417 }
418
419 +// AnyMatchWithError returns true if req matches any of the
420 +// matcher sets in ms or if there are no matchers, in which
421 +// case the request always matches. If any matcher returns
422 +// an error, we cut short and return the error.
423 +func (ms MatcherSets) AnyMatchWithError(req *http.Request) (bool, error) {
424 + for _, m := range ms {
425 + match, err := m.MatchWithError(req)
426 + if err != nil || match {
427 + return match, err
428 + }
429 + }
430 + return len(ms) == 0, nil
431 +}
432 +
433 // FromInterface fills ms from an 'any' value obtained from LoadModule.
434 func (ms *MatcherSets) FromInterface(matcherSets any) error {
435 for _, matcherSetIfaces := range matcherSets.([]map[string]any) {
436 var matcherSet MatcherSet
437 for _, matcher := range matcherSetIfaces {
383 - reqMatcher, ok := matcher.(RequestMatcher)
384 - if !ok {
385 - return fmt.Errorf("decoded module is not a RequestMatcher: %#v", matcher)
438 + if m, ok := matcher.(RequestMatcherWithError); ok {
439 + matcherSet = append(matcherSet, m)
440 + continue
441 + }
442 + if m, ok := matcher.(RequestMatcher); ok {
443 + matcherSet = append(matcherSet, m)
444 + continue
445 }
387 - matcherSet = append(matcherSet, reqMatcher)
446 + return fmt.Errorf("decoded module is not a RequestMatcher or RequestMatcherWithError: %#v", matcher)
447 }
448 *ms = append(*ms, matcherSet)
449 }
modules/caddyhttp/vars.go
+26 -12
@@ -166,8 +166,14 @@ func (m *VarsMatcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
166 // Match matches a request based on variables in the context,
167 // or placeholders if the key is not a variable.
168 func (m VarsMatcher) Match(r *http.Request) bool {
169 + match, _ := m.MatchWithError(r)
170 + return match
171 +}
172 +
173 +// MatchWithError returns true if r matches m.
174 +func (m VarsMatcher) MatchWithError(r *http.Request) (bool, error) {
175 if len(m) == 0 {
170 - return true
176 + return true, nil
177 }
178
179 vars := r.Context().Value(VarsCtxKey).(map[string]any)
@@ -200,11 +206,11 @@ func (m VarsMatcher) Match(r *http.Request) bool {
206 varStr = fmt.Sprintf("%v", vv)
207 }
208 if varStr == matcherValExpanded {
203 - return true
209 + return true, nil
210 }
211 }
212 }
207 - return false
213 + return false, nil
214 }
215
216 // CELLibrary produces options that expose this matcher for use in CEL
@@ -219,7 +225,7 @@ func (VarsMatcher) CELLibrary(_ caddy.Context) (cel.Library, error) {
225 "vars",
226 "vars_matcher_request_map",
227 []*cel.Type{CELTypeJSON},
222 - func(data ref.Val) (RequestMatcher, error) {
228 + func(data ref.Val) (RequestMatcherWithError, error) {
229 mapStrListStr, err := CELValueToMapStrList(data)
230 if err != nil {
231 return nil, err
@@ -294,6 +300,12 @@ func (m MatchVarsRE) Provision(ctx caddy.Context) error {
300
301 // Match returns true if r matches m.
302 func (m MatchVarsRE) Match(r *http.Request) bool {
303 + match, _ := m.MatchWithError(r)
304 + return match
305 +}
306 +
307 +// MatchWithError returns true if r matches m.
308 +func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
309 vars := r.Context().Value(VarsCtxKey).(map[string]any)
310 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
311 for key, val := range m {
@@ -322,10 +334,10 @@ func (m MatchVarsRE) Match(r *http.Request) bool {
334
335 valExpanded := repl.ReplaceAll(varStr, "")
336 if match := val.Match(valExpanded, repl); match {
325 - return match
337 + return match, nil
338 }
339 }
328 - return false
340 + return false, nil
341 }
342
343 // CELLibrary produces options that expose this matcher for use in CEL
@@ -340,7 +352,7 @@ func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
352 "vars_regexp",
353 "vars_regexp_request_string_string",
354 []*cel.Type{cel.StringType, cel.StringType},
343 - func(data ref.Val) (RequestMatcher, error) {
355 + func(data ref.Val) (RequestMatcherWithError, error) {
356 refStringList := reflect.TypeOf([]string{})
357 params, err := data.ConvertToNative(refStringList)
358 if err != nil {
@@ -363,7 +375,7 @@ func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
375 "vars_regexp",
376 "vars_regexp_request_string_string_string",
377 []*cel.Type{cel.StringType, cel.StringType, cel.StringType},
366 - func(data ref.Val) (RequestMatcher, error) {
378 + func(data ref.Val) (RequestMatcherWithError, error) {
379 refStringList := reflect.TypeOf([]string{})
380 params, err := data.ConvertToNative(refStringList)
381 if err != nil {
@@ -435,8 +447,10 @@ func SetVar(ctx context.Context, key string, value any) {
447
448 // Interface guards
449 var (
438 - _ MiddlewareHandler = (*VarsMiddleware)(nil)
439 - _ caddyfile.Unmarshaler = (*VarsMiddleware)(nil)
440 - _ RequestMatcher = (*VarsMatcher)(nil)
441 - _ caddyfile.Unmarshaler = (*VarsMatcher)(nil)
450 + _ MiddlewareHandler = (*VarsMiddleware)(nil)
451 + _ caddyfile.Unmarshaler = (*VarsMiddleware)(nil)
452 + _ RequestMatcherWithError = (*VarsMatcher)(nil)
453 + _ caddyfile.Unmarshaler = (*VarsMatcher)(nil)
454 + _ RequestMatcherWithError = (*MatchVarsRE)(nil)
455 + _ caddyfile.Unmarshaler = (*MatchVarsRE)(nil)
456 )
modules/caddytls/connpolicy.go
+3 -3
@@ -535,21 +535,21 @@ type ClientAuthentication struct {
535 CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"`
536 ca CA
537
538 - // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.inline` module instead.
538 + // Deprecated: Use the `ca` field with the `tls.ca_pool.source.inline` module instead.
539 // A list of base64 DER-encoded CA certificates
540 // against which to validate client certificates.
541 // Client certs which are not signed by any of
542 // these CAs will be rejected.
543 TrustedCACerts []string `json:"trusted_ca_certs,omitempty"`
544
545 - // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.file` module instead.
545 + // Deprecated: Use the `ca` field with the `tls.ca_pool.source.file` module instead.
546 // TrustedCACertPEMFiles is a list of PEM file names
547 // from which to load certificates of trusted CAs.
548 // Client certificates which are not signed by any of
549 // these CA certificates will be rejected.
550 TrustedCACertPEMFiles []string `json:"trusted_ca_certs_pem_files,omitempty"`
551
552 - // DEPRECATED: This field is deprecated and will be removed in
552 + // Deprecated: This field is deprecated and will be removed in
553 // a future version. Please use the `validators` field instead
554 // with the tls.client_auth.verifier.leaf module instead.
555 //
modules/caddytls/ondemand.go
+1 -1
@@ -42,7 +42,7 @@ func init() {
42 // to your application whether a particular domain is allowed
43 // to have a certificate issued for it.
44 type OnDemandConfig struct {
45 - // DEPRECATED. WILL BE REMOVED SOON. Use 'permission' instead with the `http` module.
45 + // Deprecated. WILL BE REMOVED SOON. Use 'permission' instead with the `http` module.
46 Ask string `json:"ask,omitempty"`
47
48 // REQUIRED. A module that will determine whether a