namesilo.go raw

   1  // Package namesilo implements a DNS provider for solving the DNS-01 challenge using namesilo DNS.
   2  package namesilo
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"time"
   9  
  10  	"github.com/go-acme/lego/v4/challenge"
  11  	"github.com/go-acme/lego/v4/challenge/dns01"
  12  	"github.com/go-acme/lego/v4/platform/config/env"
  13  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  14  	"github.com/nrdcg/namesilo"
  15  )
  16  
  17  // Environment variables names.
  18  const (
  19  	envNamespace = "NAMESILO_"
  20  
  21  	EnvAPIKey = envNamespace + "API_KEY"
  22  
  23  	EnvTTL                = envNamespace + "TTL"
  24  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  25  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  26  )
  27  
  28  const (
  29  	defaultTTL = 3600
  30  	maxTTL     = 2592000
  31  )
  32  
  33  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  34  
  35  // Config is used to configure the creation of the DNSProvider.
  36  type Config struct {
  37  	APIKey             string
  38  	PropagationTimeout time.Duration
  39  	PollingInterval    time.Duration
  40  	TTL                int
  41  }
  42  
  43  // NewDefaultConfig returns a default configuration for the DNSProvider.
  44  func NewDefaultConfig() *Config {
  45  	return &Config{
  46  		TTL:                env.GetOrDefaultInt(EnvTTL, defaultTTL),
  47  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  48  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  49  	}
  50  }
  51  
  52  // DNSProvider implements the challenge.Provider interface.
  53  type DNSProvider struct {
  54  	client *namesilo.Client
  55  	config *Config
  56  }
  57  
  58  // NewDNSProvider returns a DNSProvider instance configured for namesilo.
  59  // API_KEY must be passed in the environment variables: NAMESILO_API_KEY.
  60  //
  61  // See: https://www.namesilo.com/api_reference.php
  62  func NewDNSProvider() (*DNSProvider, error) {
  63  	values, err := env.Get(EnvAPIKey)
  64  	if err != nil {
  65  		return nil, fmt.Errorf("namesilo: %w", err)
  66  	}
  67  
  68  	config := NewDefaultConfig()
  69  	config.APIKey = values[EnvAPIKey]
  70  
  71  	return NewDNSProviderConfig(config)
  72  }
  73  
  74  // NewDNSProviderConfig return a DNSProvider instance configured for Namesilo.
  75  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  76  	if config == nil {
  77  		return nil, errors.New("namesilo: the configuration of the DNS provider is nil")
  78  	}
  79  
  80  	if config.TTL < defaultTTL || config.TTL > maxTTL {
  81  		return nil, fmt.Errorf("namesilo: TTL should be in [%d, %d]", defaultTTL, maxTTL)
  82  	}
  83  
  84  	if config.APIKey == "" {
  85  		return nil, errors.New("namesilo: credentials missing")
  86  	}
  87  
  88  	client := namesilo.NewClient(config.APIKey)
  89  
  90  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
  91  
  92  	return &DNSProvider{client: client, config: config}, nil
  93  }
  94  
  95  // Present creates a TXT record to fulfill the dns-01 challenge.
  96  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
  97  	info := dns01.GetChallengeInfo(domain, keyAuth)
  98  
  99  	zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 100  	if err != nil {
 101  		return fmt.Errorf("namesilo: could not find zone for domain %q: %w", domain, err)
 102  	}
 103  
 104  	zoneName := dns01.UnFqdn(zone)
 105  
 106  	subdomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zoneName)
 107  	if err != nil {
 108  		return fmt.Errorf("namesilo: %w", err)
 109  	}
 110  
 111  	_, err = d.client.DnsAddRecord(context.Background(), &namesilo.DnsAddRecordParams{
 112  		Domain: zoneName,
 113  		Type:   "TXT",
 114  		Host:   subdomain,
 115  		Value:  info.Value,
 116  		TTL:    d.config.TTL,
 117  	})
 118  	if err != nil {
 119  		return fmt.Errorf("namesilo: failed to add record %w", err)
 120  	}
 121  
 122  	return nil
 123  }
 124  
 125  // CleanUp removes the TXT record matching the specified parameters.
 126  func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {
 127  	ctx := context.Background()
 128  
 129  	info := dns01.GetChallengeInfo(domain, keyAuth)
 130  
 131  	zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 132  	if err != nil {
 133  		return fmt.Errorf("namesilo: could not find zone for domain %q: %w", domain, err)
 134  	}
 135  
 136  	zoneName := dns01.UnFqdn(zone)
 137  
 138  	resp, err := d.client.DnsListRecords(ctx, &namesilo.DnsListRecordsParams{Domain: zoneName})
 139  	if err != nil {
 140  		return fmt.Errorf("namesilo: %w", err)
 141  	}
 142  
 143  	subdomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zoneName)
 144  	if err != nil {
 145  		return fmt.Errorf("namesilo: %w", err)
 146  	}
 147  
 148  	for _, r := range resp.Reply.ResourceRecord {
 149  		if r.Type == "TXT" && r.Value == info.Value && (r.Host == subdomain || r.Host == dns01.UnFqdn(info.EffectiveFQDN)) {
 150  			_, err := d.client.DnsDeleteRecord(ctx, &namesilo.DnsDeleteRecordParams{Domain: zoneName, ID: r.RecordID})
 151  			if err != nil {
 152  				return fmt.Errorf("namesilo: %w", err)
 153  			}
 154  
 155  			return nil
 156  		}
 157  	}
 158  
 159  	return fmt.Errorf("namesilo: no TXT record to delete for %s (%s)", info.EffectiveFQDN, info.Value)
 160  }
 161  
 162  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 163  // Adjusting here to cope with spikes in propagation times.
 164  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 165  	return d.config.PropagationTimeout, d.config.PollingInterval
 166  }
 167