yandex360.go raw

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