keyhelp.go raw

   1  // Package keyhelp implements a DNS provider for solving the DNS-01 challenge using KeyHelp.
   2  package keyhelp
   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/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/keyhelp/internal"
  16  )
  17  
  18  // Environment variables names.
  19  const (
  20  	envNamespace = "KEYHELP_"
  21  
  22  	EnvBaseURL = envNamespace + "BASE_URL"
  23  	EnvAPIKey  = envNamespace + "API_KEY"
  24  
  25  	EnvTTL                = envNamespace + "TTL"
  26  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  27  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  28  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  29  )
  30  
  31  // Config is used to configure the creation of the DNSProvider.
  32  type Config struct {
  33  	BaseURL string
  34  	APIKey  string
  35  
  36  	PropagationTimeout time.Duration
  37  	PollingInterval    time.Duration
  38  	TTL                int
  39  	HTTPClient         *http.Client
  40  }
  41  
  42  // NewDefaultConfig returns a default configuration for the DNSProvider.
  43  func NewDefaultConfig() *Config {
  44  	return &Config{
  45  		TTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
  46  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  47  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  48  		HTTPClient: &http.Client{
  49  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
  50  		},
  51  	}
  52  }
  53  
  54  // DNSProvider implements the challenge.Provider interface.
  55  type DNSProvider struct {
  56  	config *Config
  57  	client *internal.Client
  58  
  59  	domainIDs   map[string]int
  60  	domainIDsMu sync.Mutex
  61  }
  62  
  63  // NewDNSProvider returns a DNSProvider instance configured for KeyHelp.
  64  func NewDNSProvider() (*DNSProvider, error) {
  65  	values, err := env.Get(EnvBaseURL, EnvAPIKey)
  66  	if err != nil {
  67  		return nil, fmt.Errorf("keyhelp: %w", err)
  68  	}
  69  
  70  	config := NewDefaultConfig()
  71  	config.BaseURL = values[EnvBaseURL]
  72  	config.APIKey = values[EnvAPIKey]
  73  
  74  	return NewDNSProviderConfig(config)
  75  }
  76  
  77  // NewDNSProviderConfig return a DNSProvider instance configured for KeyHelp.
  78  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  79  	if config == nil {
  80  		return nil, errors.New("keyhelp: the configuration of the DNS provider is nil")
  81  	}
  82  
  83  	client, err := internal.NewClient(config.BaseURL, config.APIKey)
  84  	if err != nil {
  85  		return nil, fmt.Errorf("keyhelp: %w", err)
  86  	}
  87  
  88  	if config.HTTPClient != nil {
  89  		client.HTTPClient = config.HTTPClient
  90  	}
  91  
  92  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
  93  
  94  	return &DNSProvider{
  95  		config:    config,
  96  		client:    client,
  97  		domainIDs: make(map[string]int),
  98  	}, nil
  99  }
 100  
 101  // Present creates a TXT record using the specified parameters.
 102  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 103  	info := dns01.GetChallengeInfo(domain, keyAuth)
 104  
 105  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 106  	if err != nil {
 107  		return fmt.Errorf("keyhelp: could not find zone for domain %q: %w", domain, err)
 108  	}
 109  
 110  	ctx := context.Background()
 111  
 112  	domainInfo, err := d.findDomain(ctx, dns01.UnFqdn(authZone))
 113  	if err != nil {
 114  		return fmt.Errorf("keyhelp: %w", err)
 115  	}
 116  
 117  	domainRecords, err := d.client.ListDomainRecords(ctx, domainInfo.ID)
 118  	if err != nil {
 119  		return fmt.Errorf("keyhelp: list domain records: %w", err)
 120  	}
 121  
 122  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
 123  	if err != nil {
 124  		return fmt.Errorf("keyhelp: %w", err)
 125  	}
 126  
 127  	records := domainRecords.Records.Other
 128  	records = append(records, internal.Record{
 129  		Host:  subDomain,
 130  		TTL:   d.config.TTL,
 131  		Type:  "TXT",
 132  		Value: info.Value,
 133  	})
 134  
 135  	req := internal.DomainRecords{
 136  		DkimRecord: domainRecords.DkimRecord,
 137  		Records: &internal.Records{
 138  			Soa:   domainRecords.Records.Soa,
 139  			Other: records,
 140  		},
 141  	}
 142  
 143  	_, err = d.client.UpdateDomainRecords(ctx, domainInfo.ID, req)
 144  	if err != nil {
 145  		return fmt.Errorf("keyhelp: update domain records (add): %w", err)
 146  	}
 147  
 148  	d.domainIDsMu.Lock()
 149  	d.domainIDs[token] = domainInfo.ID
 150  	d.domainIDsMu.Unlock()
 151  
 152  	return nil
 153  }
 154  
 155  // CleanUp removes the TXT record matching the specified parameters.
 156  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 157  	ctx := context.Background()
 158  
 159  	info := dns01.GetChallengeInfo(domain, keyAuth)
 160  
 161  	// get the domain's unique ID from when we created it
 162  	d.domainIDsMu.Lock()
 163  	domainID, ok := d.domainIDs[token]
 164  	d.domainIDsMu.Unlock()
 165  
 166  	if !ok {
 167  		return fmt.Errorf("keyhelp: unknown record ID for '%s'", info.EffectiveFQDN)
 168  	}
 169  
 170  	domainRecords, err := d.client.ListDomainRecords(ctx, domainID)
 171  	if err != nil {
 172  		return fmt.Errorf("keyhelp: list domain records: %w", err)
 173  	}
 174  
 175  	var records []internal.Record
 176  
 177  	for _, record := range domainRecords.Records.Other {
 178  		if record.Type == "TXT" && record.Value == info.Value {
 179  			continue
 180  		}
 181  
 182  		records = append(records, record)
 183  	}
 184  
 185  	req := internal.DomainRecords{
 186  		DkimRecord: domainRecords.DkimRecord,
 187  		Records: &internal.Records{
 188  			Soa:   domainRecords.Records.Soa,
 189  			Other: records,
 190  		},
 191  	}
 192  
 193  	_, err = d.client.UpdateDomainRecords(ctx, domainID, req)
 194  	if err != nil {
 195  		return fmt.Errorf("keyhelp: update domain records (delete): %w", err)
 196  	}
 197  
 198  	// Delete domain ID from map
 199  	d.domainIDsMu.Lock()
 200  	delete(d.domainIDs, token)
 201  	d.domainIDsMu.Unlock()
 202  
 203  	return nil
 204  }
 205  
 206  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 207  // Adjusting here to cope with spikes in propagation times.
 208  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 209  	return d.config.PropagationTimeout, d.config.PollingInterval
 210  }
 211  
 212  func (d *DNSProvider) findDomain(ctx context.Context, zone string) (internal.Domain, error) {
 213  	domains, err := d.client.ListDomains(ctx)
 214  	if err != nil {
 215  		return internal.Domain{}, fmt.Errorf("list domains: %w", err)
 216  	}
 217  
 218  	for _, domain := range domains {
 219  		if domain.DomainUTF8 == zone || domain.Domain == zone {
 220  			return domain, nil
 221  		}
 222  	}
 223  
 224  	return internal.Domain{}, fmt.Errorf("domain not found: %s", zone)
 225  }
 226