vinyldns.go raw

   1  // Package vinyldns implements a DNS provider for solving the DNS-01 challenge using VinylDNS.
   2  package vinyldns
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"strconv"
  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/platform/config/env"
  15  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  16  	"github.com/go-acme/lego/v4/providers/dns/internal/useragent"
  17  	"github.com/vinyldns/go-vinyldns/vinyldns"
  18  )
  19  
  20  // Environment variables names.
  21  const (
  22  	envNamespace = "VINYLDNS_"
  23  
  24  	EnvAccessKey  = envNamespace + "ACCESS_KEY"
  25  	EnvSecretKey  = envNamespace + "SECRET_KEY"
  26  	EnvHost       = envNamespace + "HOST"
  27  	EnvQuoteValue = envNamespace + "QUOTE_VALUE"
  28  
  29  	EnvTTL                = envNamespace + "TTL"
  30  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  31  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  32  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  33  )
  34  
  35  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  36  
  37  // Config is used to configure the creation of the DNSProvider.
  38  type Config struct {
  39  	AccessKey  string
  40  	SecretKey  string
  41  	Host       string
  42  	QuoteValue bool
  43  
  44  	TTL                int
  45  	PropagationTimeout time.Duration
  46  	PollingInterval    time.Duration
  47  	HTTPClient         *http.Client
  48  }
  49  
  50  // NewDefaultConfig returns a default configuration for the DNSProvider.
  51  func NewDefaultConfig() *Config {
  52  	return &Config{
  53  		TTL:                env.GetOrDefaultInt(EnvTTL, 30),
  54  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
  55  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second),
  56  		HTTPClient: &http.Client{
  57  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
  58  		},
  59  	}
  60  }
  61  
  62  // DNSProvider implements the challenge.Provider interface.
  63  type DNSProvider struct {
  64  	client *vinyldns.Client
  65  	config *Config
  66  }
  67  
  68  // NewDNSProvider returns a DNSProvider instance configured for VinylDNS.
  69  // Credentials must be passed in the environment variables:
  70  // VINYLDNS_ACCESS_KEY, VINYLDNS_SECRET_KEY, VINYLDNS_HOST.
  71  func NewDNSProvider() (*DNSProvider, error) {
  72  	values, err := env.Get(EnvAccessKey, EnvSecretKey, EnvHost)
  73  	if err != nil {
  74  		return nil, fmt.Errorf("vinyldns: %w", err)
  75  	}
  76  
  77  	config := NewDefaultConfig()
  78  	config.AccessKey = values[EnvAccessKey]
  79  	config.SecretKey = values[EnvSecretKey]
  80  	config.Host = values[EnvHost]
  81  	config.QuoteValue = env.GetOrDefaultBool(EnvQuoteValue, false)
  82  
  83  	return NewDNSProviderConfig(config)
  84  }
  85  
  86  // NewDNSProviderConfig return a DNSProvider instance configured for VinylDNS.
  87  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  88  	if config == nil {
  89  		return nil, errors.New("vinyldns: the configuration of the VinylDNS DNS provider is nil")
  90  	}
  91  
  92  	if config.AccessKey == "" || config.SecretKey == "" {
  93  		return nil, errors.New("vinyldns: credentials are missing")
  94  	}
  95  
  96  	if config.Host == "" {
  97  		return nil, errors.New("vinyldns: host is missing")
  98  	}
  99  
 100  	client := vinyldns.NewClient(vinyldns.ClientConfiguration{
 101  		AccessKey: config.AccessKey,
 102  		SecretKey: config.SecretKey,
 103  		Host:      config.Host,
 104  		UserAgent: useragent.Get(),
 105  	})
 106  
 107  	if config.HTTPClient != nil {
 108  		client.HTTPClient = config.HTTPClient
 109  	} else {
 110  		// For compatibility, it should be removed in v5.
 111  		client.HTTPClient.Timeout = 30 * time.Second
 112  	}
 113  
 114  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
 115  
 116  	return &DNSProvider{client: client, config: config}, nil
 117  }
 118  
 119  // Present creates a TXT record to fulfill the dns-01 challenge.
 120  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 121  	ctx := context.Background()
 122  
 123  	info := dns01.GetChallengeInfo(domain, keyAuth)
 124  
 125  	existingRecord, err := d.getRecordSet(info.EffectiveFQDN)
 126  	if err != nil {
 127  		return fmt.Errorf("vinyldns: %w", err)
 128  	}
 129  
 130  	value := d.formatValue(info.Value)
 131  
 132  	record := vinyldns.Record{Text: value}
 133  
 134  	if existingRecord == nil || existingRecord.ID == "" {
 135  		err = d.createRecordSet(ctx, info.EffectiveFQDN, []vinyldns.Record{record})
 136  		if err != nil {
 137  			return fmt.Errorf("vinyldns: %w", err)
 138  		}
 139  
 140  		return nil
 141  	}
 142  
 143  	for _, i := range existingRecord.Records {
 144  		if i.Text == value {
 145  			return nil
 146  		}
 147  	}
 148  
 149  	records := existingRecord.Records
 150  	records = append(records, record)
 151  
 152  	err = d.updateRecordSet(ctx, existingRecord, records)
 153  	if err != nil {
 154  		return fmt.Errorf("vinyldns: %w", err)
 155  	}
 156  
 157  	return nil
 158  }
 159  
 160  // CleanUp removes the TXT record matching the specified parameters.
 161  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 162  	ctx := context.Background()
 163  
 164  	info := dns01.GetChallengeInfo(domain, keyAuth)
 165  
 166  	existingRecord, err := d.getRecordSet(info.EffectiveFQDN)
 167  	if err != nil {
 168  		return fmt.Errorf("vinyldns: %w", err)
 169  	}
 170  
 171  	if existingRecord == nil || existingRecord.ID == "" || len(existingRecord.Records) == 0 {
 172  		return nil
 173  	}
 174  
 175  	value := d.formatValue(info.Value)
 176  
 177  	var records []vinyldns.Record
 178  
 179  	for _, i := range existingRecord.Records {
 180  		if i.Text != value {
 181  			records = append(records, i)
 182  		}
 183  	}
 184  
 185  	if len(records) == 0 {
 186  		err = d.deleteRecordSet(ctx, existingRecord)
 187  		if err != nil {
 188  			return fmt.Errorf("vinyldns: %w", err)
 189  		}
 190  
 191  		return nil
 192  	}
 193  
 194  	err = d.updateRecordSet(ctx, existingRecord, records)
 195  	if err != nil {
 196  		return fmt.Errorf("vinyldns: %w", err)
 197  	}
 198  
 199  	return nil
 200  }
 201  
 202  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 203  // Adjusting here to cope with spikes in propagation times.
 204  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 205  	return d.config.PropagationTimeout, d.config.PollingInterval
 206  }
 207  
 208  func (d *DNSProvider) formatValue(v string) string {
 209  	if d.config.QuoteValue {
 210  		return strconv.Quote(v)
 211  	}
 212  
 213  	return v
 214  }
 215