diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index d0cf30d8b..b2521c09c 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -175,10 +175,11 @@ type MatchRegexp struct { compiled *regexp.Regexp } -// Provision compiles the regular expression which may include placeholders. +// Provision compiles the regular expression which may include known placeholders. +// Unknown placeholders are preserved so regexp quantifiers remain intact. func (mre *MatchRegexp) Provision(caddy.Context) error { repl := caddy.NewReplacer() - re, err := regexp.Compile(repl.ReplaceAll(mre.Pattern, "")) + re, err := regexp.Compile(repl.ReplaceKnown(mre.Pattern, "")) if err != nil { return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err) } diff --git a/modules/caddytls/matchers_quantifier_test.go b/modules/caddytls/matchers_quantifier_test.go new file mode 100644 index 000000000..af6732ff8 --- /dev/null +++ b/modules/caddytls/matchers_quantifier_test.go @@ -0,0 +1,58 @@ +package caddytls + +import ( + "context" + "crypto/tls" + "crypto/x509" + "testing" + + "github.com/caddyserver/caddy/v2" +) + +func TestServerNameRegexpQuantifiers(t *testing.T) { + t.Setenv("CADDY_TEST_SNI_SUFFIX", "example") + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + for _, tc := range []struct { + pattern, input string + match bool + }{ + {`^a{2}\.example$`, "aa.example", true}, + {`^a{2}\.example$`, "a.example", false}, + {`^a{2,}\.example$`, "aaa.example", true}, + {`^a{2,}\.example$`, "a.example", false}, + {`^a{2,3}\.example$`, "aaa.example", true}, + {`^a{2,3}\.example$`, "a.example", false}, + {`^a{2,3}\.example$`, "aaaa.example", false}, + {`^a{2}\.{env.CADDY_TEST_SNI_SUFFIX}$`, "aa.example", true}, + {`^a{unknown}\.example$`, "a.example", false}, + } { + t.Run(tc.pattern+"/"+tc.input, func(t *testing.T) { + matcher := &MatchServerNameRE{MatchRegexp: MatchRegexp{Pattern: tc.pattern}} + if err := matcher.Provision(ctx); err != nil { + t.Fatal(err) + } + public := &tls.Config{MinVersion: tls.VersionTLS12} + protected := &tls.Config{ + MinVersion: tls.VersionTLS12, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: x509.NewCertPool(), + } + policies := ConnectionPolicies{ + {matchers: []ConnectionMatcher{matcher}, TLSConfig: public}, + {TLSConfig: protected}, + } + selected, err := policies.TLSConfig(ctx).GetConfigForClient(&tls.ClientHelloInfo{ServerName: tc.input}) + if err != nil { + t.Fatal(err) + } + want := protected + if tc.match { + want = public + } + if selected != want { + t.Fatalf("selected client authentication %v, want %v", selected.ClientAuth, want.ClientAuth) + } + }) + } +}