provider.go raw

   1  // Package selectel implements a DNS provider for solving the DNS-01 challenge using Selectel Domains API.
   2  package selectel
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"net/url"
  10  	"time"
  11  
  12  	"github.com/go-acme/lego/v4/challenge"
  13  	"github.com/go-acme/lego/v4/challenge/dns01"
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  15  	"github.com/go-acme/lego/v4/providers/dns/internal/selectel/internal"
  16  )
  17  
  18  const MinTTL = 60
  19  
  20  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  21  
  22  // Config is used to configure the creation of the DNSProvider.
  23  type Config struct {
  24  	Token              string
  25  	PropagationTimeout time.Duration
  26  	PollingInterval    time.Duration
  27  	TTL                int
  28  	HTTPClient         *http.Client
  29  
  30  	// TODO(ldez): remove in v5?
  31  	BaseURL string
  32  }
  33  
  34  // DNSProvider implements the challenge.Provider interface.
  35  type DNSProvider struct {
  36  	config *Config
  37  	client *internal.Client
  38  }
  39  
  40  // NewDNSProviderConfig return a DNSProvider instance configured for selectel.
  41  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  42  	if config == nil {
  43  		return nil, errors.New("the configuration of the DNS provider is nil")
  44  	}
  45  
  46  	if config.Token == "" {
  47  		return nil, errors.New("credentials missing")
  48  	}
  49  
  50  	if config.TTL < MinTTL {
  51  		return nil, fmt.Errorf("invalid TTL, TTL (%d) must be greater than %d", config.TTL, MinTTL)
  52  	}
  53  
  54  	client := internal.NewClient(config.Token)
  55  
  56  	if config.HTTPClient != nil {
  57  		client.HTTPClient = config.HTTPClient
  58  	}
  59  
  60  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
  61  
  62  	var err error
  63  
  64  	client.BaseURL, err = url.Parse(config.BaseURL)
  65  	if err != nil {
  66  		return nil, fmt.Errorf("%w", err)
  67  	}
  68  
  69  	return &DNSProvider{config: config, client: client}, nil
  70  }
  71  
  72  // Timeout returns the Timeout and interval to use when checking for DNS propagation.
  73  // Adjusting here to cope with spikes in propagation times.
  74  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
  75  	return d.config.PropagationTimeout, d.config.PollingInterval
  76  }
  77  
  78  // Present creates a TXT record to fulfill DNS-01 challenge.
  79  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
  80  	info := dns01.GetChallengeInfo(domain, keyAuth)
  81  
  82  	ctx := context.Background()
  83  
  84  	// TODO(ldez) replace domain by FQDN to follow CNAME.
  85  	domainObj, err := d.client.GetDomainByName(ctx, domain)
  86  	if err != nil {
  87  		return fmt.Errorf("get domain by name: %w", err)
  88  	}
  89  
  90  	txtRecord := internal.Record{
  91  		Type:    "TXT",
  92  		TTL:     d.config.TTL,
  93  		Name:    info.EffectiveFQDN,
  94  		Content: info.Value,
  95  	}
  96  
  97  	_, err = d.client.AddRecord(ctx, domainObj.ID, txtRecord)
  98  	if err != nil {
  99  		return fmt.Errorf("add record: %w", err)
 100  	}
 101  
 102  	return nil
 103  }
 104  
 105  // CleanUp removes a TXT record used for DNS-01 challenge.
 106  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 107  	info := dns01.GetChallengeInfo(domain, keyAuth)
 108  
 109  	recordName := dns01.UnFqdn(info.EffectiveFQDN)
 110  
 111  	ctx := context.Background()
 112  
 113  	// TODO(ldez) replace domain by FQDN to follow CNAME.
 114  	domainObj, err := d.client.GetDomainByName(ctx, domain)
 115  	if err != nil {
 116  		return fmt.Errorf("%w", err)
 117  	}
 118  
 119  	records, err := d.client.ListRecords(ctx, domainObj.ID)
 120  	if err != nil {
 121  		return fmt.Errorf("list records: %w", err)
 122  	}
 123  
 124  	// Delete records with specific FQDN
 125  	var lastErr error
 126  
 127  	for _, record := range records {
 128  		if record.Name == recordName {
 129  			err = d.client.DeleteRecord(ctx, domainObj.ID, record.ID)
 130  			if err != nil {
 131  				lastErr = fmt.Errorf("delete record: %w", err)
 132  			}
 133  		}
 134  	}
 135  
 136  	return lastErr
 137  }
 138