selfhostde.go raw

   1  // Package selfhostde implements a DNS provider for solving the DNS-01 challenge using SelfHost.(de|eu).
   2  package selfhostde
   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/selfhostde/internal"
  18  )
  19  
  20  // Environment variables.
  21  const (
  22  	envNamespace = "SELFHOSTDE_"
  23  
  24  	EnvUsername       = envNamespace + "USERNAME"
  25  	EnvPassword       = envNamespace + "PASSWORD"
  26  	EnvRecordsMapping = envNamespace + "RECORDS_MAPPING"
  27  
  28  	EnvTTL                = envNamespace + "TTL"
  29  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  30  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  31  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  32  )
  33  
  34  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  35  
  36  // Config is used to configure the creation of the DNSProvider.
  37  type Config struct {
  38  	Username string
  39  	Password string
  40  
  41  	RecordsMapping   map[string]*Seq
  42  	recordsMappingMu sync.Mutex
  43  
  44  	TTL                int
  45  	PropagationTimeout time.Duration
  46  	PollingInterval    time.Duration
  47  	HTTPClient         *http.Client
  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, 4*time.Minute),
  55  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 30*time.Second),
  56  		HTTPClient: &http.Client{
  57  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
  58  		},
  59  	}
  60  }
  61  
  62  func (c *Config) getSeqNext(domain string) (string, error) {
  63  	effectiveDomain := strings.TrimPrefix(domain, "_acme-challenge.")
  64  
  65  	c.recordsMappingMu.Lock()
  66  	defer c.recordsMappingMu.Unlock()
  67  
  68  	seq, ok := c.RecordsMapping[effectiveDomain]
  69  	if !ok {
  70  		// fallback
  71  		seq, ok = c.RecordsMapping[domain]
  72  		if !ok {
  73  			return "", fmt.Errorf("record mapping not found for %q", effectiveDomain)
  74  		}
  75  	}
  76  
  77  	return seq.Next(), nil
  78  }
  79  
  80  // DNSProvider implements the challenge.Provider interface.
  81  type DNSProvider struct {
  82  	config *Config
  83  	client *internal.Client
  84  
  85  	recordIDs   map[string]string
  86  	recordIDsMu sync.Mutex
  87  }
  88  
  89  // NewDNSProvider returns a DNSProvider instance configured for SelfHost.(de|eu).
  90  func NewDNSProvider() (*DNSProvider, error) {
  91  	values, err := env.Get(EnvUsername, EnvPassword, EnvRecordsMapping)
  92  	if err != nil {
  93  		return nil, fmt.Errorf("selfhostde: %w", err)
  94  	}
  95  
  96  	config := NewDefaultConfig()
  97  	config.Username = values[EnvUsername]
  98  	config.Password = values[EnvPassword]
  99  
 100  	mapping, err := parseRecordsMapping(values[EnvRecordsMapping])
 101  	if err != nil {
 102  		return nil, fmt.Errorf("selfhostde: malformed records mapping: %w", err)
 103  	}
 104  
 105  	config.RecordsMapping = mapping
 106  
 107  	return NewDNSProviderConfig(config)
 108  }
 109  
 110  // NewDNSProviderConfig return a DNSProvider instance configured for SelfHost.(de|eu).
 111  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
 112  	if config == nil {
 113  		return nil, errors.New("selfhostde: supplied configuration is nil")
 114  	}
 115  
 116  	if config.Username == "" || config.Password == "" {
 117  		return nil, errors.New("selfhostde: credentials missing")
 118  	}
 119  
 120  	if len(config.RecordsMapping) == 0 {
 121  		return nil, errors.New("selfhostde: missing record mapping")
 122  	}
 123  
 124  	for domain, seq := range config.RecordsMapping {
 125  		if seq == nil || len(seq.ids) == 0 {
 126  			return nil, fmt.Errorf("selfhostde: missing record ID for %q", domain)
 127  		}
 128  	}
 129  
 130  	client := internal.NewClient(config.Username, config.Password)
 131  
 132  	if config.HTTPClient != nil {
 133  		client.HTTPClient = config.HTTPClient
 134  	}
 135  
 136  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
 137  
 138  	return &DNSProvider{
 139  		config:    config,
 140  		client:    client,
 141  		recordIDs: make(map[string]string),
 142  	}, nil
 143  }
 144  
 145  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 146  // Adjusting here to cope with spikes in propagation times.
 147  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 148  	return d.config.PropagationTimeout, d.config.PollingInterval
 149  }
 150  
 151  // Present creates a TXT record to fulfill the dns-01 challenge.
 152  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 153  	info := dns01.GetChallengeInfo(domain, keyAuth)
 154  
 155  	recordID, err := d.config.getSeqNext(dns01.UnFqdn(info.EffectiveFQDN))
 156  	if err != nil {
 157  		return fmt.Errorf("selfhostde: %w", err)
 158  	}
 159  
 160  	err = d.client.UpdateTXTRecord(context.Background(), recordID, info.Value)
 161  	if err != nil {
 162  		return fmt.Errorf("selfhostde: update DNS TXT record (id=%s): %w", recordID, err)
 163  	}
 164  
 165  	d.recordIDsMu.Lock()
 166  	d.recordIDs[token] = recordID
 167  	d.recordIDsMu.Unlock()
 168  
 169  	return nil
 170  }
 171  
 172  // CleanUp removes the TXT record previously created.
 173  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 174  	info := dns01.GetChallengeInfo(domain, keyAuth)
 175  
 176  	d.recordIDsMu.Lock()
 177  	recordID, ok := d.recordIDs[token]
 178  	d.recordIDsMu.Unlock()
 179  
 180  	if !ok {
 181  		return fmt.Errorf("selfhostde: unknown record ID for %q", dns01.UnFqdn(info.EffectiveFQDN))
 182  	}
 183  
 184  	err := d.client.UpdateTXTRecord(context.Background(), recordID, "empty")
 185  	if err != nil {
 186  		return fmt.Errorf("selfhostde: emptied DNS TXT record (id=%s): %w", recordID, err)
 187  	}
 188  
 189  	d.recordIDsMu.Lock()
 190  	delete(d.recordIDs, token)
 191  	d.recordIDsMu.Unlock()
 192  
 193  	return nil
 194  }
 195