timewebcloud.go raw

   1  // Package timewebcloud implements a DNS provider for solving the DNS-01 challenge using Timeweb Cloud.
   2  package timewebcloud
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"sync"
  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/timewebcloud/internal"
  17  )
  18  
  19  // Environment variables names.
  20  const (
  21  	envNamespace = "TIMEWEBCLOUD_"
  22  
  23  	EnvAuthToken = envNamespace + "AUTH_TOKEN"
  24  
  25  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  26  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  27  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  28  )
  29  
  30  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  31  
  32  // Config is used to configure the creation of the DNSProvider.
  33  type Config struct {
  34  	AuthToken string
  35  
  36  	HTTPClient         *http.Client
  37  	PropagationTimeout time.Duration
  38  	PollingInterval    time.Duration
  39  }
  40  
  41  // NewDefaultConfig returns a default configuration for the DNSProvider.
  42  func NewDefaultConfig() *Config {
  43  	return &Config{
  44  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  45  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  46  		HTTPClient: &http.Client{
  47  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
  48  		},
  49  	}
  50  }
  51  
  52  // DNSProvider implements the challenge.Provider interface.
  53  type DNSProvider struct {
  54  	config *Config
  55  	client *internal.Client
  56  
  57  	recordIDs   map[string]int
  58  	recordIDsMu sync.Mutex
  59  }
  60  
  61  // NewDNSProvider returns a DNSProvider instance configured for Timeweb Cloud.
  62  // API token must be passed in the environment variable TIMEWEBCLOUD_TOKEN.
  63  func NewDNSProvider() (*DNSProvider, error) {
  64  	values, err := env.Get(EnvAuthToken)
  65  	if err != nil {
  66  		return nil, fmt.Errorf("timewebcloud: %w", err)
  67  	}
  68  
  69  	config := NewDefaultConfig()
  70  	config.AuthToken = values[EnvAuthToken]
  71  
  72  	return NewDNSProviderConfig(config)
  73  }
  74  
  75  // NewDNSProviderConfig returns a DNSProvider instance configured for Timeweb Cloud.
  76  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  77  	if config == nil {
  78  		return nil, errors.New("timewebcloud: the configuration of the DNS provider is nil")
  79  	}
  80  
  81  	if config.AuthToken == "" {
  82  		return nil, errors.New("timewebcloud: authentication token is missing")
  83  	}
  84  
  85  	client := internal.NewClient(
  86  		clientdebug.Wrap(
  87  			internal.OAuthStaticAccessToken(config.HTTPClient, config.AuthToken),
  88  		),
  89  	)
  90  
  91  	return &DNSProvider{
  92  		config:    config,
  93  		client:    client,
  94  		recordIDs: make(map[string]int),
  95  	}, nil
  96  }
  97  
  98  // Timeout returns the timeout and interval to use when checking for DNS propagation.
  99  // Adjusting here to cope with spikes in propagation times.
 100  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 101  	return d.config.PropagationTimeout, d.config.PollingInterval
 102  }
 103  
 104  // Present creates a TXT record to fulfill the dns-01 challenge.
 105  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 106  	info := dns01.GetChallengeInfo(domain, keyAuth)
 107  
 108  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 109  	if err != nil {
 110  		return fmt.Errorf("timewebcloud: could not find zone for domain %q: %w", domain, err)
 111  	}
 112  
 113  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
 114  	if err != nil {
 115  		return fmt.Errorf("timewebcloud: %w", err)
 116  	}
 117  
 118  	record := internal.DNSRecord{
 119  		Type:      "TXT",
 120  		Value:     info.Value,
 121  		SubDomain: subDomain,
 122  	}
 123  
 124  	response, err := d.client.CreateRecord(context.Background(), authZone, record)
 125  	if err != nil {
 126  		return fmt.Errorf("timewebcloud: %w", err)
 127  	}
 128  
 129  	d.recordIDsMu.Lock()
 130  	d.recordIDs[token] = response.ID
 131  	d.recordIDsMu.Unlock()
 132  
 133  	return nil
 134  }
 135  
 136  // CleanUp removes the TXT record matching the specified parameters.
 137  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 138  	info := dns01.GetChallengeInfo(domain, keyAuth)
 139  
 140  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 141  	if err != nil {
 142  		return fmt.Errorf("timewebcloud: could not find zone for domain %q: %w", domain, err)
 143  	}
 144  
 145  	d.recordIDsMu.Lock()
 146  	recordID, ok := d.recordIDs[token]
 147  	d.recordIDsMu.Unlock()
 148  
 149  	if !ok {
 150  		return fmt.Errorf("timewebcloud: unknown record ID for '%s'", info.EffectiveFQDN)
 151  	}
 152  
 153  	err = d.client.DeleteRecord(context.Background(), authZone, recordID)
 154  	if err != nil {
 155  		return fmt.Errorf("timewebcloud: %w", err)
 156  	}
 157  
 158  	d.recordIDsMu.Lock()
 159  	delete(d.recordIDs, token)
 160  	d.recordIDsMu.Unlock()
 161  
 162  	return nil
 163  }
 164