luadns.go raw

   1  // Package luadns implements a DNS provider for solving the DNS-01 challenge using LuaDNS.
   2  package luadns
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"strings"
  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/luadns/internal"
  18  )
  19  
  20  // Environment variables names.
  21  const (
  22  	envNamespace = "LUADNS_"
  23  
  24  	EnvAPIUsername = envNamespace + "API_USERNAME"
  25  	EnvAPIToken    = envNamespace + "API_TOKEN"
  26  
  27  	EnvTTL                = envNamespace + "TTL"
  28  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  29  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  30  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  31  )
  32  
  33  const minTTL = 300
  34  
  35  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  36  
  37  // Config is used to configure the creation of the DNSProvider.
  38  type Config struct {
  39  	APIUsername        string
  40  	APIToken           string
  41  	PropagationTimeout time.Duration
  42  	PollingInterval    time.Duration
  43  	TTL                int
  44  	HTTPClient         *http.Client
  45  }
  46  
  47  // NewDefaultConfig returns a default configuration for the DNSProvider.
  48  func NewDefaultConfig() *Config {
  49  	return &Config{
  50  		TTL:                env.GetOrDefaultInt(EnvTTL, minTTL),
  51  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second),
  52  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  53  		HTTPClient: &http.Client{
  54  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
  55  		},
  56  	}
  57  }
  58  
  59  // DNSProvider implements the challenge.Provider interface.
  60  type DNSProvider struct {
  61  	config *Config
  62  	client *internal.Client
  63  
  64  	recordsMu sync.Mutex
  65  	records   map[string]*internal.DNSRecord
  66  }
  67  
  68  // NewDNSProvider returns a DNSProvider instance configured for LuaDNS.
  69  // Credentials must be passed in the environment variables:
  70  // LUADNS_API_USERNAME and LUADNS_API_TOKEN.
  71  func NewDNSProvider() (*DNSProvider, error) {
  72  	values, err := env.Get(EnvAPIUsername, EnvAPIToken)
  73  	if err != nil {
  74  		return nil, fmt.Errorf("luadns: %w", err)
  75  	}
  76  
  77  	config := NewDefaultConfig()
  78  	config.APIUsername = values[EnvAPIUsername]
  79  	config.APIToken = values[EnvAPIToken]
  80  
  81  	return NewDNSProviderConfig(config)
  82  }
  83  
  84  // NewDNSProviderConfig return a DNSProvider instance configured for LuaDNS.
  85  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  86  	if config == nil {
  87  		return nil, errors.New("luadns: the configuration of the DNS provider is nil")
  88  	}
  89  
  90  	if config.APIUsername == "" || config.APIToken == "" {
  91  		return nil, errors.New("luadns: credentials missing")
  92  	}
  93  
  94  	if config.TTL < minTTL {
  95  		return nil, fmt.Errorf("luadns: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
  96  	}
  97  
  98  	client := internal.NewClient(config.APIUsername, config.APIToken)
  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.DNSRecord),
 110  	}, nil
 111  }
 112  
 113  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 114  // Adjusting here to cope with spikes in propagation times.
 115  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 116  	return d.config.PropagationTimeout, d.config.PollingInterval
 117  }
 118  
 119  // Present creates a TXT record using the specified parameters.
 120  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 121  	info := dns01.GetChallengeInfo(domain, keyAuth)
 122  
 123  	ctx := context.Background()
 124  
 125  	zones, err := d.client.ListZones(ctx)
 126  	if err != nil {
 127  		return fmt.Errorf("luadns: failed to get zones: %w", err)
 128  	}
 129  
 130  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 131  	if err != nil {
 132  		return fmt.Errorf("luadns: could not find zone for domain %q: %w", domain, err)
 133  	}
 134  
 135  	zone := findZone(zones, dns01.UnFqdn(authZone))
 136  	if zone == nil {
 137  		return fmt.Errorf("luadns: no matching zone found for domain %s", domain)
 138  	}
 139  
 140  	newRecord := internal.DNSRecord{
 141  		Name:    info.EffectiveFQDN,
 142  		Type:    "TXT",
 143  		Content: info.Value,
 144  		TTL:     d.config.TTL,
 145  	}
 146  
 147  	record, err := d.client.CreateRecord(ctx, *zone, newRecord)
 148  	if err != nil {
 149  		return fmt.Errorf("luadns: failed to create record: %w", err)
 150  	}
 151  
 152  	d.recordsMu.Lock()
 153  	d.records[token] = record
 154  	d.recordsMu.Unlock()
 155  
 156  	return nil
 157  }
 158  
 159  // CleanUp removes the TXT record matching the specified parameters.
 160  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 161  	info := dns01.GetChallengeInfo(domain, keyAuth)
 162  
 163  	d.recordsMu.Lock()
 164  	record, ok := d.records[token]
 165  	d.recordsMu.Unlock()
 166  
 167  	if !ok {
 168  		return fmt.Errorf("luadns: unknown record ID for '%s'", info.EffectiveFQDN)
 169  	}
 170  
 171  	err := d.client.DeleteRecord(context.Background(), record)
 172  	if err != nil {
 173  		return fmt.Errorf("luadns: failed to delete record: %w", err)
 174  	}
 175  
 176  	// Delete record from map
 177  	d.recordsMu.Lock()
 178  	delete(d.records, token)
 179  	d.recordsMu.Unlock()
 180  
 181  	return nil
 182  }
 183  
 184  func findZone(zones []internal.DNSZone, domain string) *internal.DNSZone {
 185  	var result *internal.DNSZone
 186  
 187  	for _, zone := range zones {
 188  		if zone.Name != "" && strings.HasSuffix(domain, zone.Name) {
 189  			if result == nil || len(zone.Name) > len(result.Name) {
 190  				result = &zone
 191  			}
 192  		}
 193  	}
 194  
 195  	return result
 196  }
 197