2019-03-11 16:56:48 +00:00
|
|
|
package dns01
|
2018-12-06 21:50:17 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
2019-02-09 04:02:58 +00:00
|
|
|
"os"
|
|
|
|
"strconv"
|
2018-12-06 21:50:17 +00:00
|
|
|
"time"
|
|
|
|
|
2020-09-02 01:20:01 +00:00
|
|
|
"github.com/go-acme/lego/v4/acme"
|
|
|
|
"github.com/go-acme/lego/v4/acme/api"
|
|
|
|
"github.com/go-acme/lego/v4/challenge"
|
|
|
|
"github.com/go-acme/lego/v4/log"
|
|
|
|
"github.com/go-acme/lego/v4/platform/wait"
|
2019-02-09 04:02:58 +00:00
|
|
|
"github.com/miekg/dns"
|
2018-12-06 21:50:17 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2020-08-09 14:39:44 +00:00
|
|
|
// DefaultPropagationTimeout default propagation timeout.
|
2018-12-06 21:50:17 +00:00
|
|
|
DefaultPropagationTimeout = 60 * time.Second
|
|
|
|
|
2020-08-09 14:39:44 +00:00
|
|
|
// DefaultPollingInterval default polling interval.
|
2018-12-06 21:50:17 +00:00
|
|
|
DefaultPollingInterval = 2 * time.Second
|
|
|
|
|
2020-08-09 14:39:44 +00:00
|
|
|
// DefaultTTL default TTL.
|
2018-12-06 21:50:17 +00:00
|
|
|
DefaultTTL = 120
|
|
|
|
)
|
|
|
|
|
|
|
|
type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error
|
|
|
|
|
|
|
|
type ChallengeOption func(*Challenge) error
|
|
|
|
|
|
|
|
// CondOption Conditional challenge option.
|
|
|
|
func CondOption(condition bool, opt ChallengeOption) ChallengeOption {
|
|
|
|
if !condition {
|
|
|
|
// NoOp options
|
|
|
|
return func(*Challenge) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return opt
|
|
|
|
}
|
|
|
|
|
2020-05-08 17:35:25 +00:00
|
|
|
// Challenge implements the dns-01 challenge.
|
2018-12-06 21:50:17 +00:00
|
|
|
type Challenge struct {
|
|
|
|
core *api.Core
|
|
|
|
validate ValidateFunc
|
|
|
|
provider challenge.Provider
|
|
|
|
preCheck preCheck
|
|
|
|
dnsTimeout time.Duration
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider, opts ...ChallengeOption) *Challenge {
|
|
|
|
chlg := &Challenge{
|
|
|
|
core: core,
|
|
|
|
validate: validate,
|
|
|
|
provider: provider,
|
|
|
|
preCheck: newPreCheck(),
|
|
|
|
dnsTimeout: 10 * time.Second,
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
err := opt(chlg)
|
|
|
|
if err != nil {
|
|
|
|
log.Infof("challenge option error: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return chlg
|
|
|
|
}
|
|
|
|
|
|
|
|
// PreSolve just submits the txt record to the dns provider.
|
|
|
|
// It does not validate record propagation, or do anything at all with the acme server.
|
|
|
|
func (c *Challenge) PreSolve(authz acme.Authorization) error {
|
|
|
|
domain := challenge.GetTargetedDomain(authz)
|
|
|
|
log.Infof("[%s] acme: Preparing to solve DNS-01", domain)
|
|
|
|
|
|
|
|
chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.provider == nil {
|
|
|
|
return fmt.Errorf("[%s] acme: no DNS Provider configured", domain)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate the Key Authorization for the challenge
|
|
|
|
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth)
|
|
|
|
if err != nil {
|
2020-02-27 18:14:46 +00:00
|
|
|
return fmt.Errorf("[%s] acme: error presenting token: %w", domain, err)
|
2018-12-06 21:50:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Challenge) Solve(authz acme.Authorization) error {
|
|
|
|
domain := challenge.GetTargetedDomain(authz)
|
|
|
|
log.Infof("[%s] acme: Trying to solve DNS-01", domain)
|
|
|
|
|
|
|
|
chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate the Key Authorization for the challenge
|
|
|
|
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fqdn, value := GetRecord(authz.Identifier.Value, keyAuth)
|
|
|
|
|
|
|
|
var timeout, interval time.Duration
|
|
|
|
switch provider := c.provider.(type) {
|
|
|
|
case challenge.ProviderTimeout:
|
|
|
|
timeout, interval = provider.Timeout()
|
|
|
|
default:
|
|
|
|
timeout, interval = DefaultPropagationTimeout, DefaultPollingInterval
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Infof("[%s] acme: Checking DNS record propagation using %+v", domain, recursiveNameservers)
|
|
|
|
|
2020-06-27 12:46:26 +00:00
|
|
|
time.Sleep(interval)
|
|
|
|
|
2018-12-21 23:53:05 +00:00
|
|
|
err = wait.For("propagation", timeout, interval, func() (bool, error) {
|
2019-02-12 16:36:44 +00:00
|
|
|
stop, errP := c.preCheck.call(domain, fqdn, value)
|
2018-12-06 21:50:17 +00:00
|
|
|
if !stop || errP != nil {
|
|
|
|
log.Infof("[%s] acme: Waiting for DNS record propagation.", domain)
|
|
|
|
}
|
|
|
|
return stop, errP
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
chlng.KeyAuthorization = keyAuth
|
2019-01-26 00:11:45 +00:00
|
|
|
return c.validate(c.core, domain, chlng)
|
2018-12-06 21:50:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// CleanUp cleans the challenge.
|
|
|
|
func (c *Challenge) CleanUp(authz acme.Authorization) error {
|
2019-01-03 15:59:53 +00:00
|
|
|
log.Infof("[%s] acme: Cleaning DNS-01 challenge", challenge.GetTargetedDomain(authz))
|
|
|
|
|
2018-12-06 21:50:17 +00:00
|
|
|
chlng, err := challenge.FindChallenge(challenge.DNS01, authz)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
keyAuth, err := c.core.GetKeyAuthorization(chlng.Token)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Challenge) Sequential() (bool, time.Duration) {
|
|
|
|
if p, ok := c.provider.(sequential); ok {
|
|
|
|
return ok, p.Sequential()
|
|
|
|
}
|
|
|
|
return false, 0
|
|
|
|
}
|
|
|
|
|
|
|
|
type sequential interface {
|
|
|
|
Sequential() time.Duration
|
|
|
|
}
|
|
|
|
|
2020-05-08 17:35:25 +00:00
|
|
|
// GetRecord returns a DNS record which will fulfill the `dns-01` challenge.
|
2020-07-09 23:48:18 +00:00
|
|
|
func GetRecord(domain, keyAuth string) (fqdn, value string) {
|
2018-12-06 21:50:17 +00:00
|
|
|
keyAuthShaBytes := sha256.Sum256([]byte(keyAuth))
|
|
|
|
// base64URL encoding without padding
|
|
|
|
value = base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size])
|
2022-09-19 09:21:35 +00:00
|
|
|
|
|
|
|
fqdn = getChallengeFqdn(domain)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func getChallengeFqdn(domain string) string {
|
|
|
|
fqdn := fmt.Sprintf("_acme-challenge.%s.", domain)
|
|
|
|
|
|
|
|
if ok, _ := strconv.ParseBool(os.Getenv("LEGO_DISABLE_CNAME_SUPPORT")); ok {
|
|
|
|
return fqdn
|
|
|
|
}
|
|
|
|
|
|
|
|
// recursion counter so it doesn't spin out of control
|
|
|
|
for limit := 0; limit < 50; limit++ {
|
|
|
|
// Keep following CNAMEs
|
|
|
|
r, err := dnsQuery(fqdn, dns.TypeCNAME, recursiveNameservers, true)
|
|
|
|
|
2022-10-10 18:43:33 +00:00
|
|
|
if err != nil || r.Rcode != dns.RcodeSuccess {
|
|
|
|
// No more CNAME records to follow, exit
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2022-09-19 09:21:35 +00:00
|
|
|
// Check if the domain has CNAME then use that
|
2022-10-10 18:43:33 +00:00
|
|
|
cname := updateDomainWithCName(r, fqdn)
|
|
|
|
if cname == fqdn {
|
|
|
|
break
|
2019-02-09 04:02:58 +00:00
|
|
|
}
|
2022-09-19 09:21:35 +00:00
|
|
|
|
2022-11-25 17:41:38 +00:00
|
|
|
log.Infof("Found CNAME entry for %q: %q", fqdn, cname)
|
|
|
|
|
2022-10-10 18:43:33 +00:00
|
|
|
fqdn = cname
|
2019-02-09 04:02:58 +00:00
|
|
|
}
|
|
|
|
|
2022-09-19 09:21:35 +00:00
|
|
|
return fqdn
|
2018-12-06 21:50:17 +00:00
|
|
|
}
|