Skip to content

Commit 09b2cbc

Browse files
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
1 parent a3481f8 commit 09b2cbc

16 files changed

+536
-202
lines changed

caddyconfig/httpcaddyfile/httptype.go

+13-8
Original file line numberDiff line numberDiff line change
@@ -1466,9 +1466,9 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
14661466

14671467
// iterate each pairing of host and path matchers and
14681468
// put them into a map for JSON encoding
1469-
var matcherSets []map[string]caddyhttp.RequestMatcher
1469+
var matcherSets []map[string]caddyhttp.RequestMatcherWithError
14701470
for _, mp := range matcherPairs {
1471-
matcherSet := make(map[string]caddyhttp.RequestMatcher)
1471+
matcherSet := make(map[string]caddyhttp.RequestMatcherWithError)
14721472
if len(mp.hostm) > 0 {
14731473
matcherSet["host"] = mp.hostm
14741474
}
@@ -1527,12 +1527,17 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
15271527
if err != nil {
15281528
return err
15291529
}
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
15331534
}
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)
15361541
}
15371542

15381543
// 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
15761581
return nil
15771582
}
15781583

1579-
func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.ModuleMap, error) {
1584+
func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcherWithError) (caddy.ModuleMap, error) {
15801585
msEncoded := make(caddy.ModuleMap)
15811586
for matcherName, val := range matchers {
15821587
jsonBytes, err := json.Marshal(val)

modules/caddyhttp/caddyhttp.go

+16
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,26 @@ func init() {
3636
// RequestMatcher is a type that can match to a request.
3737
// A route matcher MUST NOT modify the request, with the
3838
// 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.
3943
type RequestMatcher interface {
4044
Match(*http.Request) bool
4145
}
4246

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+
4359
// Handler is like http.Handler except ServeHTTP may return an error.
4460
//
4561
// If any handler encounters an error, it should be returned for proper

modules/caddyhttp/celmatcher.go

+102-33
Original file line numberDiff line numberDiff line change
@@ -202,17 +202,25 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
202202

203203
// Match returns true if r matches m.
204204
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) {
205214
celReq := celHTTPRequest{r}
206215
out, _, err := m.prg.Eval(celReq)
207216
if err != nil {
208217
m.log.Error("evaluating expression", zap.Error(err))
209-
SetVar(r.Context(), MatcherErrorVarKey, err)
210-
return false
218+
return false, err
211219
}
212220
if outBool, ok := out.Value().(bool); ok {
213-
return outBool
221+
return outBool, nil
214222
}
215-
return false
223+
return false, nil
216224
}
217225

218226
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
@@ -380,7 +388,7 @@ type CELLibraryProducer interface {
380388
// limited set of function signatures. For strong type validation you may need
381389
// to provide a custom macro which does a more detailed analysis of the CEL
382390
// 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) {
384392
requestType := cel.ObjectType("http.Request")
385393
var macro parser.Macro
386394
switch len(matcherDataTypes) {
@@ -424,7 +432,11 @@ func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fa
424432
}
425433

426434
// 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)
428440

429441
// matcherCELLibrary is a simplistic configurable cel.Library implementation.
430442
type matcherCELLibrary struct {
@@ -452,7 +464,7 @@ func (lib *matcherCELLibrary) ProgramOptions() []cel.ProgramOption {
452464
// that takes a single argument, and optimizes the implementation to precompile
453465
// the matcher and return a function that references the precompiled and
454466
// provisioned matcher.
455-
func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.InterpretableDecorator {
467+
func CELMatcherDecorator(funcName string, fac any) interpreter.InterpretableDecorator {
456468
return func(i interpreter.Interpretable) (interpreter.Interpretable, error) {
457469
call, ok := i.(interpreter.InterpretableCall)
458470
if !ok {
@@ -481,35 +493,92 @@ func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.Int
481493
// and matcher provisioning should be handled at dynamically.
482494
return i, nil
483495
}
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
487518
}
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)
500547
}
501548
}
502549

503550
// CELMatcherRuntimeFunction creates a function binding for when the input to the matcher
504551
// 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 {
506553
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))
510580
}
511-
httpReq := celReq.Value().(celHTTPRequest)
512-
return types.Bool(matcher.Match(httpReq.Request))
581+
return types.NewErr("CELMatcherRuntimeFunction invalid matcher factory: %T", fac)
513582
}
514583
}
515584

@@ -733,9 +802,9 @@ const MatcherNameCtxKey = "matcher_name"
733802

734803
// Interface guards
735804
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)
741810
)

modules/caddyhttp/celmatcher_test.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,11 @@ func TestMatchExpressionMatch(t *testing.T) {
489489
}
490490
}
491491

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 {
493497
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
494498
}
495499
})
@@ -532,7 +536,7 @@ func BenchmarkMatchExpressionMatch(b *testing.B) {
532536
}
533537
b.ResetTimer()
534538
for i := 0; i < b.N; i++ {
535-
tc.expression.Match(req)
539+
tc.expression.MatchWithError(req)
536540
}
537541
})
538542
}

0 commit comments

Comments
 (0)