yandex.go raw

   1  // Package yandex implements a DNS provider for solving the DNS-01 challenge using Yandex PDD.
   2  package yandex
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   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/platform/config/env"
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  15  	"github.com/go-acme/lego/v4/providers/dns/yandex/internal"
  16  	"github.com/miekg/dns"
  17  )
  18  
  19  // Environment variables names.
  20  const (
  21  	envNamespace = "YANDEX_"
  22  
  23  	EnvPddToken = envNamespace + "PDD_TOKEN"
  24  
  25  	EnvTTL                = envNamespace + "TTL"
  26  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  27  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  28  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  29  )
  30  
  31  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  32  
  33  // Config is used to configure the creation of the DNSProvider.
  34  type Config struct {
  35  	PddToken           string
  36  	PropagationTimeout time.Duration
  37  	PollingInterval    time.Duration
  38  	TTL                int
  39  	HTTPClient         *http.Client
  40  }
  41  
  42  // NewDefaultConfig returns a default configuration for the DNSProvider.
  43  func NewDefaultConfig() *Config {
  44  	return &Config{
  45  		TTL:                env.GetOrDefaultInt(EnvTTL, 21600),
  46  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  47  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  48  		HTTPClient: &http.Client{
  49  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
  50  		},
  51  	}
  52  }
  53  
  54  // DNSProvider implements the challenge.Provider interface.
  55  type DNSProvider struct {
  56  	client *internal.Client
  57  	config *Config
  58  }
  59  
  60  // NewDNSProvider returns a DNSProvider instance configured for Yandex.
  61  func NewDNSProvider() (*DNSProvider, error) {
  62  	values, err := env.Get(EnvPddToken)
  63  	if err != nil {
  64  		return nil, fmt.Errorf("yandex: %w", err)
  65  	}
  66  
  67  	config := NewDefaultConfig()
  68  	config.PddToken = values[EnvPddToken]
  69  
  70  	return NewDNSProviderConfig(config)
  71  }
  72  
  73  // NewDNSProviderConfig return a DNSProvider instance configured for Yandex.
  74  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  75  	if config == nil {
  76  		return nil, errors.New("yandex: the configuration of the DNS provider is nil")
  77  	}
  78  
  79  	if config.PddToken == "" {
  80  		return nil, errors.New("yandex: credentials missing")
  81  	}
  82  
  83  	client, err := internal.NewClient(config.PddToken)
  84  	if err != nil {
  85  		return nil, fmt.Errorf("yandex: %w", err)
  86  	}
  87  
  88  	if config.HTTPClient != nil {
  89  		client.HTTPClient = config.HTTPClient
  90  	}
  91  
  92  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
  93  
  94  	return &DNSProvider{client: client, config: config}, nil
  95  }
  96  
  97  // Present creates a TXT record to fulfill the dns-01 challenge.
  98  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
  99  	info := dns01.GetChallengeInfo(domain, keyAuth)
 100  
 101  	rootDomain, subDomain, err := splitDomain(info.EffectiveFQDN)
 102  	if err != nil {
 103  		return fmt.Errorf("yandex: %w", err)
 104  	}
 105  
 106  	data := internal.Record{
 107  		Domain:    rootDomain,
 108  		SubDomain: subDomain,
 109  		Type:      "TXT",
 110  		TTL:       d.config.TTL,
 111  		Content:   info.Value,
 112  	}
 113  
 114  	_, err = d.client.AddRecord(context.Background(), data)
 115  	if err != nil {
 116  		return fmt.Errorf("yandex: %w", err)
 117  	}
 118  
 119  	return nil
 120  }
 121  
 122  // CleanUp removes the TXT record matching the specified parameters.
 123  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 124  	info := dns01.GetChallengeInfo(domain, keyAuth)
 125  
 126  	rootDomain, subDomain, err := splitDomain(info.EffectiveFQDN)
 127  	if err != nil {
 128  		return fmt.Errorf("yandex: %w", err)
 129  	}
 130  
 131  	ctx := context.Background()
 132  
 133  	records, err := d.client.GetRecords(ctx, rootDomain)
 134  	if err != nil {
 135  		return fmt.Errorf("yandex: %w", err)
 136  	}
 137  
 138  	var record *internal.Record
 139  
 140  	for _, rcd := range records {
 141  		if rcd.Type == "TXT" && rcd.SubDomain == subDomain && rcd.Content == info.Value {
 142  			record = &rcd
 143  			break
 144  		}
 145  	}
 146  
 147  	if record == nil {
 148  		return fmt.Errorf("yandex: TXT record not found for domain: %s", domain)
 149  	}
 150  
 151  	data := internal.Record{
 152  		ID:     record.ID,
 153  		Domain: rootDomain,
 154  	}
 155  
 156  	_, err = d.client.RemoveRecord(ctx, data)
 157  	if err != nil {
 158  		return fmt.Errorf("yandex: %w", err)
 159  	}
 160  
 161  	return nil
 162  }
 163  
 164  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 165  // Adjusting here to cope with spikes in propagation times.
 166  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 167  	return d.config.PropagationTimeout, d.config.PollingInterval
 168  }
 169  
 170  func splitDomain(full string) (string, string, error) {
 171  	split := dns.Split(full)
 172  	if len(split) < 2 {
 173  		return "", "", fmt.Errorf("unsupported domain: %s", full)
 174  	}
 175  
 176  	if len(split) == 2 {
 177  		return full, "", nil
 178  	}
 179  
 180  	domain := full[split[len(split)-2]:]
 181  	subDomain := full[:split[len(split)-2]-1]
 182  
 183  	return domain, subDomain, nil
 184  }
 185