baiducloud.go raw

   1  // Package baiducloud implements a DNS provider for solving the DNS-01 challenge using Baidu Cloud.
   2  package baiducloud
   3  
   4  import (
   5  	"errors"
   6  	"fmt"
   7  	"time"
   8  
   9  	baidudns "github.com/baidubce/bce-sdk-go/services/dns"
  10  	"github.com/go-acme/lego/v4/challenge/dns01"
  11  	"github.com/go-acme/lego/v4/platform/config/env"
  12  	"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
  13  )
  14  
  15  // Environment variables names.
  16  const (
  17  	envNamespace = "BAIDUCLOUD_"
  18  
  19  	EnvAccessKeyID     = envNamespace + "ACCESS_KEY_ID"
  20  	EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY"
  21  
  22  	EnvTTL                = envNamespace + "TTL"
  23  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  24  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  25  )
  26  
  27  // 300 is the minimum TTL for free users.
  28  const defaultTTL = 300
  29  
  30  // Config is used to configure the creation of the DNSProvider.
  31  type Config struct {
  32  	AccessKeyID     string
  33  	SecretAccessKey string
  34  
  35  	PropagationTimeout time.Duration
  36  	PollingInterval    time.Duration
  37  	TTL                int
  38  }
  39  
  40  // NewDefaultConfig returns a default configuration for the DNSProvider.
  41  func NewDefaultConfig() *Config {
  42  	return &Config{
  43  		TTL:                env.GetOrDefaultInt(EnvTTL, defaultTTL),
  44  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  45  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  46  	}
  47  }
  48  
  49  // DNSProvider implements the challenge.Provider interface.
  50  type DNSProvider struct {
  51  	config *Config
  52  	client *baidudns.Client
  53  }
  54  
  55  // NewDNSProvider returns a DNSProvider instance configured for Baidu Cloud.
  56  func NewDNSProvider() (*DNSProvider, error) {
  57  	values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey)
  58  	if err != nil {
  59  		return nil, fmt.Errorf("baiducloud: %w", err)
  60  	}
  61  
  62  	config := NewDefaultConfig()
  63  	config.AccessKeyID = values[EnvAccessKeyID]
  64  	config.SecretAccessKey = values[EnvSecretAccessKey]
  65  
  66  	return NewDNSProviderConfig(config)
  67  }
  68  
  69  // NewDNSProviderConfig return a DNSProvider instance configured for Baidu Cloud.
  70  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  71  	if config == nil {
  72  		return nil, errors.New("baiducloud: the configuration of the DNS provider is nil")
  73  	}
  74  
  75  	if config.AccessKeyID == "" && config.SecretAccessKey == "" {
  76  		return nil, errors.New("baiducloud: credentials missing")
  77  	}
  78  
  79  	client, err := baidudns.NewClient(config.AccessKeyID, config.SecretAccessKey, "")
  80  	if err != nil {
  81  		return nil, fmt.Errorf("baiducloud: %w", err)
  82  	}
  83  
  84  	return &DNSProvider{
  85  		config: config,
  86  		client: client,
  87  	}, nil
  88  }
  89  
  90  // Present creates a TXT record using the specified parameters.
  91  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
  92  	info := dns01.GetChallengeInfo(domain, keyAuth)
  93  
  94  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
  95  	if err != nil {
  96  		return fmt.Errorf("baiducloud: could not find zone for domain %q: %w", domain, err)
  97  	}
  98  
  99  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
 100  	if err != nil {
 101  		return fmt.Errorf("baiducloud: %w", err)
 102  	}
 103  
 104  	crr := &baidudns.CreateRecordRequest{
 105  		Description: ptr.Pointer("lego"),
 106  		Rr:          subDomain,
 107  		Type:        "TXT",
 108  		Value:       info.Value,
 109  		Ttl:         ptr.Pointer(int32(d.config.TTL)),
 110  	}
 111  
 112  	err = d.client.CreateRecord(dns01.UnFqdn(authZone), crr, "")
 113  	if err != nil {
 114  		return fmt.Errorf("baiducloud: create record: %w", err)
 115  	}
 116  
 117  	return nil
 118  }
 119  
 120  // CleanUp removes the TXT record matching the specified parameters.
 121  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 122  	info := dns01.GetChallengeInfo(domain, keyAuth)
 123  
 124  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 125  	if err != nil {
 126  		return fmt.Errorf("baiducloud: could not find zone for domain %q: %w", domain, err)
 127  	}
 128  
 129  	recordID, err := d.findRecordID(dns01.UnFqdn(authZone), info.Value)
 130  	if err != nil {
 131  		return fmt.Errorf("baiducloud: find record: %w", err)
 132  	}
 133  
 134  	err = d.client.DeleteRecord(dns01.UnFqdn(authZone), recordID, "")
 135  	if err != nil {
 136  		return fmt.Errorf("baiducloud: delete record: %w", err)
 137  	}
 138  
 139  	return nil
 140  }
 141  
 142  func (d *DNSProvider) findRecordID(zoneName, tokenValue string) (string, error) {
 143  	lrr := &baidudns.ListRecordRequest{}
 144  
 145  	for {
 146  		recordResponse, err := d.client.ListRecord(zoneName, lrr)
 147  		if err != nil {
 148  			return "", fmt.Errorf("baiducloud: list record: %w", err)
 149  		}
 150  
 151  		for _, record := range recordResponse.Records {
 152  			if record.Type == "TXT" && record.Value == tokenValue {
 153  				return record.Id, nil
 154  			}
 155  		}
 156  
 157  		if !recordResponse.IsTruncated {
 158  			break
 159  		}
 160  
 161  		lrr.Marker = recordResponse.NextMarker
 162  	}
 163  
 164  	return "", errors.New("record not found")
 165  }
 166  
 167  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 168  // Adjusting here to cope with spikes in propagation times.
 169  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 170  	return d.config.PropagationTimeout, d.config.PollingInterval
 171  }
 172