Skip to content

Commit f1a05ab

Browse files
oldmantaitertraefiker
authored andcommitted
Add wildcard match to acme domains
1 parent 4c85a41 commit f1a05ab

File tree

7 files changed

+219
-34
lines changed

7 files changed

+219
-34
lines changed

acme/account.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ func (dc *DomainsCertificates) getCertificateForDomain(domainToFind string) (*Do
219219

220220
for _, domainsCertificate := range dc.Certs {
221221
for _, domain := range domainsCertificate.Domains.ToStrArray() {
222+
if strings.HasPrefix(domain, "*.") && types.MatchDomain(domainToFind, domain) {
223+
return domainsCertificate, true
224+
}
222225
if domain == domainToFind {
223226
return domainsCertificate, true
224227
}

acme/acme.go

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"net/http"
1212
"os"
1313
"reflect"
14-
"regexp"
1514
"strings"
1615
"time"
1716

@@ -27,7 +26,7 @@ import (
2726
"github.com/containous/traefik/tls/generate"
2827
"github.com/containous/traefik/types"
2928
"github.com/eapache/channels"
30-
acme "github.com/xenolf/lego/acmev2"
29+
"github.com/xenolf/lego/acmev2"
3130
"github.com/xenolf/lego/providers/dns"
3231
)
3332

@@ -555,15 +554,14 @@ func (a *ACME) getProvidedCertificate(domains string) *tls.Certificate {
555554
func searchProvidedCertificateForDomains(domain string, certs map[string]*tls.Certificate) *tls.Certificate {
556555
// Use regex to test for provided certs that might have been added into TLSConfig
557556
for certDomains := range certs {
558-
domainCheck := false
557+
domainChecked := false
559558
for _, certDomain := range strings.Split(certDomains, ",") {
560-
selector := "^" + strings.Replace(certDomain, "*.", "[^\\.]*\\.", -1) + "$"
561-
domainCheck, _ = regexp.MatchString(selector, domain)
562-
if domainCheck {
559+
domainChecked = types.MatchDomain(domain, certDomain)
560+
if domainChecked {
563561
break
564562
}
565563
}
566-
if domainCheck {
564+
if domainChecked {
567565
log.Debugf("Domain %q checked by provided certificate %q", domain, certDomains)
568566
return certs[certDomains]
569567
}
@@ -684,15 +682,7 @@ func (a *ACME) getValidDomains(domains []string, wildcardAllowed bool) ([]string
684682
func isDomainAlreadyChecked(domainToCheck string, existentDomains map[string]*tls.Certificate) bool {
685683
for certDomains := range existentDomains {
686684
for _, certDomain := range strings.Split(certDomains, ",") {
687-
// Use regex to test for provided existentDomains that might have been added into TLSConfig
688-
selector := "^" + strings.Replace(certDomain, "*.", "[^\\.]*\\.", -1) + "$"
689-
domainCheck, err := regexp.MatchString(selector, domainToCheck)
690-
if err != nil {
691-
log.Errorf("Unable to compare %q and %q : %s", domainToCheck, certDomain, err)
692-
continue
693-
}
694-
695-
if domainCheck {
685+
if types.MatchDomain(domainToCheck, certDomain) {
696686
return true
697687
}
698688
}

acme/acme_test.go

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"github.com/containous/traefik/tls/generate"
1515
"github.com/containous/traefik/types"
1616
"github.com/stretchr/testify/assert"
17-
acme "github.com/xenolf/lego/acmev2"
17+
"github.com/xenolf/lego/acmev2"
1818
)
1919

2020
func TestDomainsSet(t *testing.T) {
@@ -444,3 +444,93 @@ func TestAcme_getValidDomain(t *testing.T) {
444444
})
445445
}
446446
}
447+
448+
func TestAcme_getCertificateForDomain(t *testing.T) {
449+
testCases := []struct {
450+
desc string
451+
domain string
452+
dc *DomainsCertificates
453+
expected *DomainsCertificate
454+
expectedFound bool
455+
}{
456+
{
457+
desc: "non-wildcard exact match",
458+
domain: "foo.traefik.wtf",
459+
dc: &DomainsCertificates{
460+
Certs: []*DomainsCertificate{
461+
{
462+
Domains: types.Domain{
463+
Main: "foo.traefik.wtf",
464+
},
465+
},
466+
},
467+
},
468+
expected: &DomainsCertificate{
469+
Domains: types.Domain{
470+
Main: "foo.traefik.wtf",
471+
},
472+
},
473+
expectedFound: true,
474+
},
475+
{
476+
desc: "non-wildcard no match",
477+
domain: "bar.traefik.wtf",
478+
dc: &DomainsCertificates{
479+
Certs: []*DomainsCertificate{
480+
{
481+
Domains: types.Domain{
482+
Main: "foo.traefik.wtf",
483+
},
484+
},
485+
},
486+
},
487+
expected: nil,
488+
expectedFound: false,
489+
},
490+
{
491+
desc: "wildcard match",
492+
domain: "foo.traefik.wtf",
493+
dc: &DomainsCertificates{
494+
Certs: []*DomainsCertificate{
495+
{
496+
Domains: types.Domain{
497+
Main: "*.traefik.wtf",
498+
},
499+
},
500+
},
501+
},
502+
expected: &DomainsCertificate{
503+
Domains: types.Domain{
504+
Main: "*.traefik.wtf",
505+
},
506+
},
507+
expectedFound: true,
508+
},
509+
{
510+
desc: "wildcard no match",
511+
domain: "foo.traefik.wtf",
512+
dc: &DomainsCertificates{
513+
Certs: []*DomainsCertificate{
514+
{
515+
Domains: types.Domain{
516+
Main: "*.bar.traefik.wtf",
517+
},
518+
},
519+
},
520+
},
521+
expected: nil,
522+
expectedFound: false,
523+
},
524+
}
525+
526+
for _, test := range testCases {
527+
test := test
528+
t.Run(test.desc, func(t *testing.T) {
529+
t.Parallel()
530+
531+
got, found := test.dc.getCertificateForDomain(test.domain)
532+
assert.Equal(t, test.expectedFound, found)
533+
assert.Equal(t, test.expected, got)
534+
})
535+
}
536+
}

provider/acme/provider.go

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"net/http"
1111
"os"
1212
"reflect"
13-
"regexp"
1413
"strings"
1514
"sync"
1615
"time"
@@ -24,7 +23,7 @@ import (
2423
traefikTLS "github.com/containous/traefik/tls"
2524
"github.com/containous/traefik/types"
2625
"github.com/pkg/errors"
27-
acme "github.com/xenolf/lego/acmev2"
26+
"github.com/xenolf/lego/acmev2"
2827
"github.com/xenolf/lego/providers/dns"
2928
)
3029

@@ -522,7 +521,7 @@ func (p *Provider) getUncheckedDomains(domainsToCheck []string, checkConfigurati
522521
}
523522

524523
func searchUncheckedDomains(domainsToCheck []string, existentDomains []string) []string {
525-
uncheckedDomains := []string{}
524+
var uncheckedDomains []string
526525
for _, domainToCheck := range domainsToCheck {
527526
if !isDomainAlreadyChecked(domainToCheck, existentDomains) {
528527
uncheckedDomains = append(uncheckedDomains, domainToCheck)
@@ -583,14 +582,7 @@ func (p *Provider) getValidDomains(domain types.Domain, wildcardAllowed bool) ([
583582
func isDomainAlreadyChecked(domainToCheck string, existentDomains []string) bool {
584583
for _, certDomains := range existentDomains {
585584
for _, certDomain := range strings.Split(certDomains, ",") {
586-
// Use regex to test for provided existentDomains that might have been added into TLSConfig
587-
selector := "^" + strings.Replace(certDomain, "*.", "[^\\.]*\\.", -1) + "$"
588-
domainCheck, err := regexp.MatchString(selector, domainToCheck)
589-
if err != nil {
590-
log.Errorf("Unable to compare %q and %q in ACME provider : %s", domainToCheck, certDomain, err)
591-
continue
592-
}
593-
if domainCheck {
585+
if types.MatchDomain(domainToCheck, certDomain) {
594586
return true
595587
}
596588
}

server/server.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"os"
1616
"os/signal"
1717
"reflect"
18-
"regexp"
1918
"sort"
2019
"strings"
2120
"sync"
@@ -517,15 +516,13 @@ func (s *Server) loadHTTPSConfiguration(configurations types.Configurations, def
517516
return newEPCertificates, nil
518517
}
519518

520-
// getCertificate allows to customize tlsConfig.Getcertificate behaviour to get the certificates inserted dynamically
519+
// getCertificate allows to customize tlsConfig.GetCertificate behaviour to get the certificates inserted dynamically
521520
func (s *serverEntryPoint) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
522521
domainToCheck := types.CanonicalDomain(clientHello.ServerName)
523522
if s.certs.Get() != nil {
524523
for domains, cert := range s.certs.Get().(map[string]*tls.Certificate) {
525-
for _, domain := range strings.Split(domains, ",") {
526-
selector := "^" + strings.Replace(domain, "*.", "[^\\.]*\\.?", -1) + "$"
527-
domainCheck, _ := regexp.MatchString(selector, domainToCheck)
528-
if domainCheck {
524+
for _, certDomain := range strings.Split(domains, ",") {
525+
if types.MatchDomain(domainToCheck, certDomain) {
529526
return cert, nil
530527
}
531528
}

types/domain_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,95 @@ func TestDomain_Set(t *testing.T) {
8888
})
8989
}
9090
}
91+
92+
func TestMatchDomain(t *testing.T) {
93+
testCases := []struct {
94+
desc string
95+
certDomain string
96+
domain string
97+
expected bool
98+
}{
99+
{
100+
desc: "exact match",
101+
certDomain: "traefik.wtf",
102+
domain: "traefik.wtf",
103+
expected: true,
104+
},
105+
{
106+
desc: "wildcard and root domain",
107+
certDomain: "*.traefik.wtf",
108+
domain: "traefik.wtf",
109+
expected: false,
110+
},
111+
{
112+
desc: "wildcard and sub domain",
113+
certDomain: "*.traefik.wtf",
114+
domain: "sub.traefik.wtf",
115+
expected: true,
116+
},
117+
{
118+
desc: "wildcard and sub sub domain",
119+
certDomain: "*.traefik.wtf",
120+
domain: "sub.sub.traefik.wtf",
121+
expected: false,
122+
},
123+
{
124+
desc: "double wildcard and sub sub domain",
125+
certDomain: "*.*.traefik.wtf",
126+
domain: "sub.sub.traefik.wtf",
127+
expected: true,
128+
},
129+
{
130+
desc: "sub sub domain and invalid wildcard",
131+
certDomain: "sub.*.traefik.wtf",
132+
domain: "sub.sub.traefik.wtf",
133+
expected: false,
134+
},
135+
{
136+
desc: "sub sub domain and valid wildcard",
137+
certDomain: "*.sub.traefik.wtf",
138+
domain: "sub.sub.traefik.wtf",
139+
expected: true,
140+
},
141+
{
142+
desc: "dot replaced by a cahr",
143+
certDomain: "sub.sub.traefik.wtf",
144+
domain: "sub.sub.traefikiwtf",
145+
expected: false,
146+
},
147+
{
148+
desc: "*",
149+
certDomain: "*",
150+
domain: "sub.sub.traefik.wtf",
151+
expected: false,
152+
},
153+
{
154+
desc: "?",
155+
certDomain: "?",
156+
domain: "sub.sub.traefik.wtf",
157+
expected: false,
158+
},
159+
{
160+
desc: "...................",
161+
certDomain: "...................",
162+
domain: "sub.sub.traefik.wtf",
163+
expected: false,
164+
},
165+
{
166+
desc: "wildcard and *",
167+
certDomain: "*.traefik.wtf",
168+
domain: "*.*.traefik.wtf",
169+
expected: false,
170+
},
171+
}
172+
173+
for _, test := range testCases {
174+
test := test
175+
t.Run(test.desc, func(t *testing.T) {
176+
t.Parallel()
177+
178+
domains := MatchDomain(test.domain, test.certDomain)
179+
assert.Equal(t, test.expected, domains)
180+
})
181+
}
182+
}

types/domains.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,24 @@ func (ds *Domains) String() string { return fmt.Sprintf("%+v", *ds) }
6565
func (ds *Domains) SetValue(val interface{}) {
6666
*ds = val.([]Domain)
6767
}
68+
69+
// MatchDomain return true if a domain match the cert domain
70+
func MatchDomain(domain string, certDomain string) bool {
71+
if domain == certDomain {
72+
return true
73+
}
74+
75+
for len(certDomain) > 0 && certDomain[len(certDomain)-1] == '.' {
76+
certDomain = certDomain[:len(certDomain)-1]
77+
}
78+
79+
labels := strings.Split(domain, ".")
80+
for i := range labels {
81+
labels[i] = "*"
82+
candidate := strings.Join(labels, ".")
83+
if certDomain == candidate {
84+
return true
85+
}
86+
}
87+
return false
88+
}

0 commit comments

Comments
 (0)