5780dc2b15
Co-authored-by: Mathieu Lonjaret <mathieu.lonjaret@gmail.com>
173 lines
4.4 KiB
Go
173 lines
4.4 KiB
Go
package acme
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
|
"github.com/traefik/traefik/v2/pkg/config/dynamic"
|
|
"github.com/traefik/traefik/v2/pkg/log"
|
|
"github.com/traefik/traefik/v2/pkg/safe"
|
|
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
|
|
"github.com/traefik/traefik/v2/pkg/types"
|
|
)
|
|
|
|
const providerNameALPN = "tlsalpn.acme"
|
|
|
|
// ChallengeTLSALPN TLSALPN challenge provider implements challenge.Provider.
|
|
type ChallengeTLSALPN struct {
|
|
chans map[string]chan struct{}
|
|
muChans sync.Mutex
|
|
|
|
certs map[string]*Certificate
|
|
muCerts sync.Mutex
|
|
|
|
configurationChan chan<- dynamic.Message
|
|
}
|
|
|
|
// NewChallengeTLSALPN creates a new ChallengeTLSALPN.
|
|
func NewChallengeTLSALPN() *ChallengeTLSALPN {
|
|
return &ChallengeTLSALPN{
|
|
chans: make(map[string]chan struct{}),
|
|
certs: make(map[string]*Certificate),
|
|
}
|
|
}
|
|
|
|
// Present presents a challenge to obtain new ACME certificate.
|
|
func (c *ChallengeTLSALPN) Present(domain, _, keyAuth string) error {
|
|
logger := log.WithoutContext().WithField(log.ProviderName, providerNameALPN)
|
|
logger.Debugf("TLS Challenge Present temp certificate for %s", domain)
|
|
|
|
certPEMBlock, keyPEMBlock, err := tlsalpn01.ChallengeBlocks(domain, keyAuth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cert := &Certificate{Certificate: certPEMBlock, Key: keyPEMBlock, Domain: types.Domain{Main: "TEMP-" + domain}}
|
|
|
|
c.muChans.Lock()
|
|
ch := make(chan struct{})
|
|
c.chans[string(certPEMBlock)] = ch
|
|
c.muChans.Unlock()
|
|
|
|
c.muCerts.Lock()
|
|
c.certs[keyAuth] = cert
|
|
conf := createMessage(c.certs)
|
|
c.muCerts.Unlock()
|
|
|
|
c.configurationChan <- conf
|
|
|
|
// Present should return when its dynamic configuration has been received and applied by Traefik.
|
|
// The timer exists in case the above does not happen, to ensure the challenge cleanup.
|
|
timer := time.NewTimer(time.Minute)
|
|
defer timer.Stop()
|
|
|
|
select {
|
|
case t := <-timer.C:
|
|
c.muChans.Lock()
|
|
c.cleanChan(string(certPEMBlock))
|
|
c.muChans.Unlock()
|
|
|
|
err = c.CleanUp(domain, "", keyAuth)
|
|
if err != nil {
|
|
logger.Errorf("Failed to clean up TLS challenge: %v", err)
|
|
}
|
|
|
|
return fmt.Errorf("timeout %s", t)
|
|
case <-ch:
|
|
// noop
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// CleanUp cleans the challenges when certificate is obtained.
|
|
func (c *ChallengeTLSALPN) CleanUp(domain, _, keyAuth string) error {
|
|
log.WithoutContext().WithField(log.ProviderName, providerNameALPN).
|
|
Debugf("TLS Challenge CleanUp temp certificate for %s", domain)
|
|
|
|
c.muCerts.Lock()
|
|
delete(c.certs, keyAuth)
|
|
conf := createMessage(c.certs)
|
|
c.muCerts.Unlock()
|
|
|
|
c.configurationChan <- conf
|
|
|
|
return nil
|
|
}
|
|
|
|
// Init the provider.
|
|
func (c *ChallengeTLSALPN) Init() error {
|
|
return nil
|
|
}
|
|
|
|
// ThrottleDuration returns the throttle duration.
|
|
func (c *ChallengeTLSALPN) ThrottleDuration() time.Duration {
|
|
return 0
|
|
}
|
|
|
|
// Provide allows the provider to provide configurations to traefik using the given configuration channel.
|
|
func (c *ChallengeTLSALPN) Provide(configurationChan chan<- dynamic.Message, _ *safe.Pool) error {
|
|
c.configurationChan = configurationChan
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListenConfiguration sets a new Configuration into the configurationChan.
|
|
func (c *ChallengeTLSALPN) ListenConfiguration(conf dynamic.Configuration) {
|
|
c.muChans.Lock()
|
|
|
|
for _, certificate := range conf.TLS.Certificates {
|
|
if !containsACMETLS1(certificate.Stores) {
|
|
continue
|
|
}
|
|
|
|
c.cleanChan(certificate.CertFile.String())
|
|
}
|
|
|
|
c.muChans.Unlock()
|
|
}
|
|
|
|
func (c *ChallengeTLSALPN) cleanChan(key string) {
|
|
if _, ok := c.chans[key]; ok {
|
|
close(c.chans[key])
|
|
delete(c.chans, key)
|
|
}
|
|
}
|
|
|
|
func createMessage(certs map[string]*Certificate) dynamic.Message {
|
|
conf := dynamic.Message{
|
|
ProviderName: providerNameALPN,
|
|
Configuration: &dynamic.Configuration{
|
|
HTTP: &dynamic.HTTPConfiguration{
|
|
Routers: map[string]*dynamic.Router{},
|
|
Middlewares: map[string]*dynamic.Middleware{},
|
|
Services: map[string]*dynamic.Service{},
|
|
},
|
|
TLS: &dynamic.TLSConfiguration{},
|
|
},
|
|
}
|
|
|
|
for _, cert := range certs {
|
|
certConf := &traefiktls.CertAndStores{
|
|
Certificate: traefiktls.Certificate{
|
|
CertFile: traefiktls.FileOrContent(cert.Certificate),
|
|
KeyFile: traefiktls.FileOrContent(cert.Key),
|
|
},
|
|
Stores: []string{tlsalpn01.ACMETLS1Protocol},
|
|
}
|
|
conf.Configuration.TLS.Certificates = append(conf.Configuration.TLS.Certificates, certConf)
|
|
}
|
|
|
|
return conf
|
|
}
|
|
|
|
func containsACMETLS1(stores []string) bool {
|
|
for _, store := range stores {
|
|
if store == tlsalpn01.ACMETLS1Protocol {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|