stackpath.go raw

   1  // Package stackpath implements a DNS provider for solving the DNS-01 challenge using Stackpath DNS.
   2  // https://developer.stackpath.com/en/api/dns/
   3  package stackpath
   4  
   5  import (
   6  	"context"
   7  	"errors"
   8  	"fmt"
   9  	"time"
  10  
  11  	"github.com/go-acme/lego/v4/challenge"
  12  	"github.com/go-acme/lego/v4/challenge/dns01"
  13  	"github.com/go-acme/lego/v4/log"
  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/stackpath/internal"
  17  )
  18  
  19  // Environment variables names.
  20  const (
  21  	envNamespace = "STACKPATH_"
  22  
  23  	EnvClientID     = envNamespace + "CLIENT_ID"
  24  	EnvClientSecret = envNamespace + "CLIENT_SECRET"
  25  	EnvStackID      = envNamespace + "STACK_ID"
  26  
  27  	EnvTTL                = envNamespace + "TTL"
  28  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  29  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  30  )
  31  
  32  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  33  
  34  // Config is used to configure the creation of the DNSProvider.
  35  type Config struct {
  36  	ClientID           string
  37  	ClientSecret       string
  38  	StackID            string
  39  	TTL                int
  40  	PropagationTimeout time.Duration
  41  	PollingInterval    time.Duration
  42  }
  43  
  44  // NewDefaultConfig returns a default configuration for the DNSProvider.
  45  func NewDefaultConfig() *Config {
  46  	return &Config{
  47  		TTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
  48  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  49  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  50  	}
  51  }
  52  
  53  // DNSProvider implements the challenge.Provider interface.
  54  type DNSProvider struct {
  55  	config *Config
  56  	client *internal.Client
  57  }
  58  
  59  // NewDNSProvider returns a DNSProvider instance configured for Stackpath.
  60  // Credentials must be passed in the environment variables:
  61  // STACKPATH_CLIENT_ID, STACKPATH_CLIENT_SECRET, and STACKPATH_STACK_ID.
  62  func NewDNSProvider() (*DNSProvider, error) {
  63  	values, err := env.Get(EnvClientID, EnvClientSecret, EnvStackID)
  64  	if err != nil {
  65  		return nil, fmt.Errorf("stackpath: %w", err)
  66  	}
  67  
  68  	config := NewDefaultConfig()
  69  	config.ClientID = values[EnvClientID]
  70  	config.ClientSecret = values[EnvClientSecret]
  71  	config.StackID = values[EnvStackID]
  72  
  73  	return NewDNSProviderConfig(config)
  74  }
  75  
  76  // NewDNSProviderConfig return a DNSProvider instance configured for Stackpath.
  77  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  78  	if config == nil {
  79  		return nil, errors.New("stackpath: the configuration of the DNS provider is nil")
  80  	}
  81  
  82  	if config.ClientID == "" || config.ClientSecret == "" {
  83  		return nil, errors.New("stackpath: credentials missing")
  84  	}
  85  
  86  	if config.StackID == "" {
  87  		return nil, errors.New("stackpath: stack id missing")
  88  	}
  89  
  90  	return &DNSProvider{
  91  		config: config,
  92  		client: internal.NewClient(config.StackID,
  93  			clientdebug.Wrap(
  94  				internal.CreateOAuthClient(context.Background(), config.ClientID, config.ClientSecret),
  95  			),
  96  		),
  97  	}, nil
  98  }
  99  
 100  // Present creates a TXT record to fulfill the dns-01 challenge.
 101  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 102  	info := dns01.GetChallengeInfo(domain, keyAuth)
 103  
 104  	ctx := context.Background()
 105  
 106  	zone, err := d.client.GetZones(ctx, info.EffectiveFQDN)
 107  	if err != nil {
 108  		return fmt.Errorf("stackpath: %w", err)
 109  	}
 110  
 111  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Domain)
 112  	if err != nil {
 113  		return fmt.Errorf("stackpath: %w", err)
 114  	}
 115  
 116  	record := internal.Record{
 117  		Name: subDomain,
 118  		Type: "TXT",
 119  		TTL:  d.config.TTL,
 120  		Data: info.Value,
 121  	}
 122  
 123  	return d.client.CreateZoneRecord(ctx, zone, record)
 124  }
 125  
 126  // CleanUp removes the TXT record matching the specified parameters.
 127  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 128  	info := dns01.GetChallengeInfo(domain, keyAuth)
 129  
 130  	ctx := context.Background()
 131  
 132  	zone, err := d.client.GetZones(ctx, info.EffectiveFQDN)
 133  	if err != nil {
 134  		return fmt.Errorf("stackpath: %w", err)
 135  	}
 136  
 137  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Domain)
 138  	if err != nil {
 139  		return fmt.Errorf("stackpath: %w", err)
 140  	}
 141  
 142  	records, err := d.client.GetZoneRecords(ctx, subDomain, zone)
 143  	if err != nil {
 144  		return err
 145  	}
 146  
 147  	for _, record := range records {
 148  		err = d.client.DeleteZoneRecord(ctx, zone, record)
 149  		if err != nil {
 150  			log.Printf("stackpath: failed to delete TXT record: %v", err)
 151  		}
 152  	}
 153  
 154  	return nil
 155  }
 156  
 157  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 158  // Adjusting here to cope with spikes in propagation times.
 159  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 160  	return d.config.PropagationTimeout, d.config.PollingInterval
 161  }
 162