cloudflare.go raw

   1  // Package cloudflare implements a DNS provider for solving the DNS-01 challenge using cloudflare DNS.
   2  package cloudflare
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"strconv"
  10  	"strings"
  11  	"sync"
  12  	"time"
  13  
  14  	"github.com/go-acme/lego/v4/challenge"
  15  	"github.com/go-acme/lego/v4/challenge/dns01"
  16  	"github.com/go-acme/lego/v4/log"
  17  	"github.com/go-acme/lego/v4/platform/config/env"
  18  	"github.com/go-acme/lego/v4/providers/dns/cloudflare/internal"
  19  )
  20  
  21  // Environment variables names.
  22  const (
  23  	envNamespace = "CLOUDFLARE_"
  24  
  25  	EnvEmail  = envNamespace + "EMAIL"
  26  	EnvAPIKey = envNamespace + "API_KEY"
  27  
  28  	EnvDNSAPIToken  = envNamespace + "DNS_API_TOKEN"
  29  	EnvZoneAPIToken = envNamespace + "ZONE_API_TOKEN"
  30  
  31  	EnvBaseURL = envNamespace + "BASE_URL"
  32  
  33  	EnvTTL                = envNamespace + "TTL"
  34  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  35  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  36  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  37  )
  38  
  39  const (
  40  	altEnvNamespace = "CF_"
  41  
  42  	altEnvEmail = altEnvNamespace + "API_EMAIL"
  43  )
  44  
  45  const (
  46  	minTTL = 120
  47  )
  48  
  49  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  50  
  51  // Config is used to configure the creation of the DNSProvider.
  52  type Config struct {
  53  	AuthEmail string
  54  	AuthKey   string
  55  
  56  	AuthToken string
  57  	ZoneToken string
  58  
  59  	BaseURL string
  60  
  61  	TTL                int
  62  	PropagationTimeout time.Duration
  63  	PollingInterval    time.Duration
  64  	HTTPClient         *http.Client
  65  }
  66  
  67  // NewDefaultConfig returns a default configuration for the DNSProvider.
  68  func NewDefaultConfig() *Config {
  69  	return &Config{
  70  		TTL:                env.GetOneWithFallback(EnvTTL, minTTL, strconv.Atoi, altEnvName(EnvTTL)),
  71  		PropagationTimeout: env.GetOneWithFallback(EnvPropagationTimeout, 2*time.Minute, env.ParseSecond, altEnvName(EnvPropagationTimeout)),
  72  		PollingInterval:    env.GetOneWithFallback(EnvPollingInterval, dns01.DefaultPollingInterval, env.ParseSecond, altEnvName(EnvPollingInterval)),
  73  		HTTPClient: &http.Client{
  74  			Timeout: env.GetOneWithFallback(EnvHTTPTimeout, 30*time.Second, env.ParseSecond, altEnvName(EnvHTTPTimeout)),
  75  		},
  76  	}
  77  }
  78  
  79  // DNSProvider implements the challenge.Provider interface.
  80  type DNSProvider struct {
  81  	client *metaClient
  82  	config *Config
  83  
  84  	recordIDs   map[string]string
  85  	recordIDsMu sync.Mutex
  86  }
  87  
  88  // NewDNSProvider returns a DNSProvider instance configured for Cloudflare.
  89  // Credentials must be passed in as environment variables:
  90  //
  91  // Either provide CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY,
  92  // or a CLOUDFLARE_DNS_API_TOKEN.
  93  //
  94  // For a more paranoid setup, provide CLOUDFLARE_DNS_API_TOKEN and CLOUDFLARE_ZONE_API_TOKEN.
  95  //
  96  // The email and API key should be avoided, if possible.
  97  // Instead, set up an API token with both Zone:Read and DNS:Edit permission, and pass the CLOUDFLARE_DNS_API_TOKEN environment variable.
  98  // You can split the Zone:Read and DNS:Edit permissions across multiple API tokens:
  99  // in this case pass both CLOUDFLARE_ZONE_API_TOKEN and CLOUDFLARE_DNS_API_TOKEN accordingly.
 100  func NewDNSProvider() (*DNSProvider, error) {
 101  	values, err := env.GetWithFallback(
 102  		[]string{EnvEmail, altEnvEmail},
 103  		[]string{EnvAPIKey, altEnvName(EnvAPIKey)},
 104  	)
 105  	if err != nil {
 106  		var errT error
 107  
 108  		values, errT = env.GetWithFallback(
 109  			[]string{EnvDNSAPIToken, altEnvName(EnvDNSAPIToken)},
 110  			[]string{EnvZoneAPIToken, altEnvName(EnvZoneAPIToken), EnvDNSAPIToken, altEnvName(EnvDNSAPIToken)},
 111  		)
 112  		if errT != nil {
 113  			//nolint:errorlint
 114  			return nil, fmt.Errorf("cloudflare: %v or %v", err, errT)
 115  		}
 116  	}
 117  
 118  	config := NewDefaultConfig()
 119  	config.AuthEmail = values[EnvEmail]
 120  	config.AuthKey = values[EnvAPIKey]
 121  	config.AuthToken = values[EnvDNSAPIToken]
 122  	config.ZoneToken = values[EnvZoneAPIToken]
 123  	config.BaseURL = env.GetOrFile(EnvBaseURL)
 124  
 125  	return NewDNSProviderConfig(config)
 126  }
 127  
 128  // NewDNSProviderConfig return a DNSProvider instance configured for Cloudflare.
 129  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
 130  	if config == nil {
 131  		return nil, errors.New("cloudflare: the configuration of the DNS provider is nil")
 132  	}
 133  
 134  	if config.TTL < minTTL {
 135  		return nil, fmt.Errorf("cloudflare: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
 136  	}
 137  
 138  	client, err := newClient(config)
 139  	if err != nil {
 140  		return nil, fmt.Errorf("cloudflare: %w", err)
 141  	}
 142  
 143  	return &DNSProvider{
 144  		client:    client,
 145  		config:    config,
 146  		recordIDs: make(map[string]string),
 147  	}, nil
 148  }
 149  
 150  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 151  // Adjusting here to cope with spikes in propagation times.
 152  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 153  	return d.config.PropagationTimeout, d.config.PollingInterval
 154  }
 155  
 156  // Present creates a TXT record to fulfill the dns-01 challenge.
 157  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 158  	ctx := context.Background()
 159  
 160  	info := dns01.GetChallengeInfo(domain, keyAuth)
 161  
 162  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 163  	if err != nil {
 164  		return fmt.Errorf("cloudflare: could not find zone for domain %q: %w", domain, err)
 165  	}
 166  
 167  	zoneID, err := d.client.ZoneIDByName(ctx, authZone)
 168  	if err != nil {
 169  		return fmt.Errorf("cloudflare: failed to find zone %s: %w", authZone, err)
 170  	}
 171  
 172  	dnsRecord := internal.Record{
 173  		Type:    "TXT",
 174  		Name:    dns01.UnFqdn(info.EffectiveFQDN),
 175  		Content: `"` + info.Value + `"`,
 176  		TTL:     d.config.TTL,
 177  	}
 178  
 179  	response, err := d.client.CreateDNSRecord(ctx, zoneID, dnsRecord)
 180  	if err != nil {
 181  		return fmt.Errorf("cloudflare: failed to create TXT record: %w", err)
 182  	}
 183  
 184  	d.recordIDsMu.Lock()
 185  	d.recordIDs[token] = response.ID
 186  	d.recordIDsMu.Unlock()
 187  
 188  	log.Infof("cloudflare: new record for %s, ID %s", domain, response.ID)
 189  
 190  	return nil
 191  }
 192  
 193  // CleanUp removes the TXT record matching the specified parameters.
 194  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 195  	ctx := context.Background()
 196  
 197  	info := dns01.GetChallengeInfo(domain, keyAuth)
 198  
 199  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 200  	if err != nil {
 201  		return fmt.Errorf("cloudflare: could not find zone for domain %q: %w", domain, err)
 202  	}
 203  
 204  	zoneID, err := d.client.ZoneIDByName(ctx, authZone)
 205  	if err != nil {
 206  		return fmt.Errorf("cloudflare: failed to find zone %s: %w", authZone, err)
 207  	}
 208  
 209  	// get the record's unique ID from when we created it
 210  	d.recordIDsMu.Lock()
 211  	recordID, ok := d.recordIDs[token]
 212  	d.recordIDsMu.Unlock()
 213  
 214  	if !ok {
 215  		return fmt.Errorf("cloudflare: unknown record ID for '%s'", info.EffectiveFQDN)
 216  	}
 217  
 218  	err = d.client.DeleteDNSRecord(ctx, zoneID, recordID)
 219  	if err != nil {
 220  		log.Printf("cloudflare: failed to delete TXT record: %v", err)
 221  	}
 222  
 223  	// Delete record ID from map
 224  	d.recordIDsMu.Lock()
 225  	delete(d.recordIDs, token)
 226  	d.recordIDsMu.Unlock()
 227  
 228  	return nil
 229  }
 230  
 231  func altEnvName(v string) string {
 232  	return strings.ReplaceAll(v, envNamespace, altEnvNamespace)
 233  }
 234