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