cloudru.go raw

   1  // Package cloudru implements a DNS provider for solving the DNS-01 challenge using cloud.ru DNS.
   2  package cloudru
   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/cloudru/internal"
  17  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  18  )
  19  
  20  // Environment variables names.
  21  const (
  22  	envNamespace = "CLOUDRU_"
  23  
  24  	EnvServiceInstanceID = envNamespace + "SERVICE_INSTANCE_ID"
  25  	EnvKeyID             = envNamespace + "KEY_ID"
  26  	EnvSecret            = envNamespace + "SECRET"
  27  
  28  	EnvTTL                = envNamespace + "TTL"
  29  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  30  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  31  	EnvSequenceInterval   = envNamespace + "SEQUENCE_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  	ServiceInstanceID string
  40  	KeyID             string
  41  	Secret            string
  42  
  43  	PropagationTimeout time.Duration
  44  	PollingInterval    time.Duration
  45  	SequenceInterval   time.Duration
  46  	HTTPClient         *http.Client
  47  	TTL                int
  48  }
  49  
  50  // NewDefaultConfig returns a default configuration for the DNSProvider.
  51  func NewDefaultConfig() *Config {
  52  	return &Config{
  53  		TTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
  54  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
  55  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),
  56  		SequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),
  57  		HTTPClient: &http.Client{
  58  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
  59  		},
  60  	}
  61  }
  62  
  63  type DNSProvider struct {
  64  	config *Config
  65  	client *internal.Client
  66  
  67  	records   map[string]*internal.Record
  68  	recordsMu sync.Mutex
  69  }
  70  
  71  // NewDNSProvider returns a DNSProvider instance configured for cloud.ru.
  72  // Credentials must be passed in the environment variables:
  73  // CLOUDRU_SERVICE_INSTANCE_ID, CLOUDRU_KEY_ID, and CLOUDRU_SECRET.
  74  func NewDNSProvider() (*DNSProvider, error) {
  75  	values, err := env.Get(EnvServiceInstanceID, EnvKeyID, EnvSecret)
  76  	if err != nil {
  77  		return nil, fmt.Errorf("cloudru: %w", err)
  78  	}
  79  
  80  	config := NewDefaultConfig()
  81  	config.ServiceInstanceID = values[EnvServiceInstanceID]
  82  	config.KeyID = values[EnvKeyID]
  83  	config.Secret = values[EnvSecret]
  84  
  85  	return NewDNSProviderConfig(config)
  86  }
  87  
  88  // NewDNSProviderConfig return a DNSProvider instance configured for cloud.ru.
  89  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  90  	if config == nil {
  91  		return nil, errors.New("cloudru: the configuration of the DNS provider is nil")
  92  	}
  93  
  94  	if config.ServiceInstanceID == "" || config.KeyID == "" || config.Secret == "" {
  95  		return nil, errors.New("cloudru: some credentials information are missing")
  96  	}
  97  
  98  	client := internal.NewClient(config.KeyID, config.Secret)
  99  
 100  	if config.HTTPClient != nil {
 101  		client.HTTPClient = config.HTTPClient
 102  	}
 103  
 104  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
 105  
 106  	return &DNSProvider{
 107  		config:  config,
 108  		client:  client,
 109  		records: make(map[string]*internal.Record),
 110  	}, nil
 111  }
 112  
 113  // Present creates a TXT record using the specified parameters.
 114  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 115  	info := dns01.GetChallengeInfo(domain, keyAuth)
 116  
 117  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 118  	if err != nil {
 119  		return fmt.Errorf("cloudru: could not find zone for domain %q: %w", domain, err)
 120  	}
 121  
 122  	authZone = dns01.UnFqdn(authZone)
 123  
 124  	ctx, err := d.client.CreateAuthenticatedContext(context.Background())
 125  	if err != nil {
 126  		return fmt.Errorf("cloudru: %w", err)
 127  	}
 128  
 129  	zone, err := d.getZoneInformationByName(ctx, d.config.ServiceInstanceID, authZone)
 130  	if err != nil {
 131  		return fmt.Errorf("cloudru: could not find zone information (ServiceInstanceID: %s, zone: %s): %w", d.config.ServiceInstanceID, authZone, err)
 132  	}
 133  
 134  	record := internal.Record{
 135  		Name:   info.EffectiveFQDN,
 136  		Type:   "TXT",
 137  		Values: []string{info.Value},
 138  		TTL:    strconv.Itoa(d.config.TTL),
 139  	}
 140  
 141  	newRecord, err := d.client.CreateRecord(ctx, zone.ID, record)
 142  	if err != nil {
 143  		return fmt.Errorf("cloudru: could not create record: %w", err)
 144  	}
 145  
 146  	d.recordsMu.Lock()
 147  	d.records[token] = newRecord
 148  	d.recordsMu.Unlock()
 149  
 150  	return nil
 151  }
 152  
 153  // CleanUp removes a given record that was generated by Present.
 154  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 155  	info := dns01.GetChallengeInfo(domain, keyAuth)
 156  
 157  	d.recordsMu.Lock()
 158  	record, ok := d.records[token]
 159  	d.recordsMu.Unlock()
 160  
 161  	if !ok {
 162  		return fmt.Errorf("cloudru: unknown recordID for %q", info.EffectiveFQDN)
 163  	}
 164  
 165  	ctx, err := d.client.CreateAuthenticatedContext(context.Background())
 166  	if err != nil {
 167  		return fmt.Errorf("cloudru: %w", err)
 168  	}
 169  
 170  	err = d.client.DeleteRecord(ctx, record.ZoneID, record.Name, "TXT")
 171  	if err != nil {
 172  		return fmt.Errorf("cloudru: %w", err)
 173  	}
 174  
 175  	d.recordsMu.Lock()
 176  	delete(d.records, token)
 177  	d.recordsMu.Unlock()
 178  
 179  	return nil
 180  }
 181  
 182  // Sequential All DNS challenges for this provider will be resolved sequentially.
 183  // Returns the interval between each iteration.
 184  func (d *DNSProvider) Sequential() time.Duration {
 185  	return d.config.SequenceInterval
 186  }
 187  
 188  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 189  // Adjusting here to cope with spikes in propagation times.
 190  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 191  	return d.config.PropagationTimeout, d.config.PollingInterval
 192  }
 193  
 194  func (d *DNSProvider) getZoneInformationByName(ctx context.Context, parentID, name string) (internal.Zone, error) {
 195  	zs, err := d.client.GetZones(ctx, parentID)
 196  	if err != nil {
 197  		return internal.Zone{}, err
 198  	}
 199  
 200  	for _, element := range zs {
 201  		if element.Name == name {
 202  			return element, nil
 203  		}
 204  	}
 205  
 206  	return internal.Zone{}, errors.New("could not find Zone record")
 207  }
 208