otc.go raw

   1  // Package otc implements a DNS provider for solving the DNS-01 challenge using Open Telekom Cloud Managed DNS.
   2  package otc
   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/otc/internal"
  16  )
  17  
  18  // Environment variables names.
  19  const (
  20  	envNamespace = "OTC_"
  21  
  22  	EnvDomainName       = envNamespace + "DOMAIN_NAME"
  23  	EnvUserName         = envNamespace + "USER_NAME"
  24  	EnvPassword         = envNamespace + "PASSWORD"
  25  	EnvProjectName      = envNamespace + "PROJECT_NAME"
  26  	EnvIdentityEndpoint = envNamespace + "IDENTITY_ENDPOINT"
  27  	EnvPrivateZone      = envNamespace + "PRIVATE_ZONE"
  28  
  29  	EnvTTL                = envNamespace + "TTL"
  30  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  31  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  32  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  33  	EnvSequenceInterval   = envNamespace + "SEQUENCE_INTERVAL"
  34  )
  35  
  36  const defaultIdentityEndpoint = "https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens"
  37  
  38  // minTTL 300 is otc minimum value for TTL.
  39  const minTTL = 300
  40  
  41  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  42  
  43  // Config is used to configure the creation of the DNSProvider.
  44  type Config struct {
  45  	DomainName       string
  46  	ProjectName      string
  47  	UserName         string
  48  	Password         string
  49  	IdentityEndpoint string
  50  	PrivateZone      bool
  51  
  52  	PropagationTimeout time.Duration
  53  	PollingInterval    time.Duration
  54  	SequenceInterval   time.Duration
  55  	TTL                int
  56  	HTTPClient         *http.Client
  57  }
  58  
  59  // NewDefaultConfig returns a default configuration for the DNSProvider.
  60  func NewDefaultConfig() *Config {
  61  	tr := &http.Transport{}
  62  
  63  	defaultTransport, ok := http.DefaultTransport.(*http.Transport)
  64  	if ok {
  65  		tr = defaultTransport.Clone()
  66  	}
  67  
  68  	// Workaround for keep alive bug in otc api
  69  	tr.DisableKeepAlives = true
  70  
  71  	return &Config{
  72  		PrivateZone:      env.GetOrDefaultBool(EnvPrivateZone, false),
  73  		IdentityEndpoint: env.GetOrDefaultString(EnvIdentityEndpoint, defaultIdentityEndpoint),
  74  
  75  		TTL:                env.GetOrDefaultInt(EnvTTL, minTTL),
  76  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  77  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  78  		SequenceInterval:   env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),
  79  		HTTPClient: &http.Client{
  80  			Timeout:   env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
  81  			Transport: tr,
  82  		},
  83  	}
  84  }
  85  
  86  // DNSProvider implements the challenge.Provider interface.
  87  type DNSProvider struct {
  88  	config *Config
  89  	client *internal.Client
  90  }
  91  
  92  // NewDNSProvider returns a DNSProvider instance configured for OTC DNS.
  93  // Credentials must be passed in the environment variables: OTC_USER_NAME,
  94  // OTC_DOMAIN_NAME, OTC_PASSWORD OTC_PROJECT_NAME and OTC_IDENTITY_ENDPOINT.
  95  func NewDNSProvider() (*DNSProvider, error) {
  96  	values, err := env.Get(EnvDomainName, EnvUserName, EnvPassword, EnvProjectName)
  97  	if err != nil {
  98  		return nil, fmt.Errorf("otc: %w", err)
  99  	}
 100  
 101  	config := NewDefaultConfig()
 102  	config.DomainName = values[EnvDomainName]
 103  	config.UserName = values[EnvUserName]
 104  	config.Password = values[EnvPassword]
 105  	config.ProjectName = values[EnvProjectName]
 106  
 107  	return NewDNSProviderConfig(config)
 108  }
 109  
 110  // NewDNSProviderConfig return a DNSProvider instance configured for OTC DNS.
 111  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
 112  	if config == nil {
 113  		return nil, errors.New("otc: the configuration of the DNS provider is nil")
 114  	}
 115  
 116  	if config.DomainName == "" || config.UserName == "" || config.Password == "" || config.ProjectName == "" {
 117  		return nil, errors.New("otc: credentials missing")
 118  	}
 119  
 120  	if config.TTL < minTTL {
 121  		return nil, fmt.Errorf("otc: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
 122  	}
 123  
 124  	client := internal.NewClient(config.UserName, config.Password, config.DomainName, config.ProjectName)
 125  
 126  	if config.IdentityEndpoint != "" {
 127  		client.IdentityEndpoint = config.IdentityEndpoint
 128  	}
 129  
 130  	if config.HTTPClient != nil {
 131  		client.HTTPClient = config.HTTPClient
 132  	}
 133  
 134  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
 135  
 136  	return &DNSProvider{config: config, client: client}, nil
 137  }
 138  
 139  // Present creates a TXT record using the specified parameters.
 140  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 141  	info := dns01.GetChallengeInfo(domain, keyAuth)
 142  
 143  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 144  	if err != nil {
 145  		return fmt.Errorf("otc: could not find zone for domain %q: %w", domain, err)
 146  	}
 147  
 148  	ctx := context.Background()
 149  
 150  	err = d.client.Login(ctx)
 151  	if err != nil {
 152  		return fmt.Errorf("otc: %w", err)
 153  	}
 154  
 155  	zoneID, err := d.client.GetZoneID(ctx, authZone, d.config.PrivateZone)
 156  	if err != nil {
 157  		return fmt.Errorf("otc: unable to get zone: %w", err)
 158  	}
 159  
 160  	record := internal.RecordSets{
 161  		Name:        info.EffectiveFQDN,
 162  		Description: "Added TXT record for ACME dns-01 challenge using lego client",
 163  		Type:        "TXT",
 164  		TTL:         d.config.TTL,
 165  		Records:     []string{fmt.Sprintf("%q", info.Value)},
 166  	}
 167  
 168  	err = d.client.CreateRecordSet(ctx, zoneID, record)
 169  	if err != nil {
 170  		return fmt.Errorf("otc: %w", err)
 171  	}
 172  
 173  	return nil
 174  }
 175  
 176  // CleanUp removes the TXT record matching the specified parameters.
 177  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 178  	info := dns01.GetChallengeInfo(domain, keyAuth)
 179  
 180  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 181  	if err != nil {
 182  		return fmt.Errorf("otc: could not find zone for domain %q: %w", domain, err)
 183  	}
 184  
 185  	ctx := context.Background()
 186  
 187  	err = d.client.Login(ctx)
 188  	if err != nil {
 189  		return fmt.Errorf("otc: %w", err)
 190  	}
 191  
 192  	zoneID, err := d.client.GetZoneID(ctx, authZone, d.config.PrivateZone)
 193  	if err != nil {
 194  		return fmt.Errorf("otc: %w", err)
 195  	}
 196  
 197  	recordID, err := d.client.GetRecordSetID(ctx, zoneID, info.EffectiveFQDN)
 198  	if err != nil {
 199  		return fmt.Errorf("otc: unable to get record %s for zone %s: %w", info.EffectiveFQDN, domain, err)
 200  	}
 201  
 202  	err = d.client.DeleteRecordSet(ctx, zoneID, recordID)
 203  	if err != nil {
 204  		return fmt.Errorf("otc: %w", err)
 205  	}
 206  
 207  	return nil
 208  }
 209  
 210  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 211  // Adjusting here to cope with spikes in propagation times.
 212  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 213  	return d.config.PropagationTimeout, d.config.PollingInterval
 214  }
 215  
 216  // Sequential All DNS challenges for this provider will be resolved sequentially.
 217  // Returns the interval between each iteration.
 218  func (d *DNSProvider) Sequential() time.Duration {
 219  	return d.config.SequenceInterval
 220  }
 221