metaname.go raw

   1  // Package metaname implements a DNS provider for solving the DNS-01 challenge using Metaname.
   2  package metaname
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"sync"
   9  	"time"
  10  
  11  	"github.com/go-acme/lego/v4/challenge"
  12  	"github.com/go-acme/lego/v4/challenge/dns01"
  13  	"github.com/go-acme/lego/v4/platform/config/env"
  14  	"github.com/nzdjb/go-metaname"
  15  )
  16  
  17  // Environment variables names.
  18  const (
  19  	envNamespace = "METANAME_"
  20  
  21  	EnvAccountReference = envNamespace + "ACCOUNT_REFERENCE"
  22  	EnvAPIKey           = envNamespace + "API_KEY"
  23  
  24  	EnvTTL                = envNamespace + "TTL"
  25  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  26  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  27  )
  28  
  29  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  30  
  31  // Config is used to configure the creation of the DNSProvider.
  32  type Config struct {
  33  	AccountReference   string
  34  	APIKey             string
  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  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  44  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  45  		TTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
  46  	}
  47  }
  48  
  49  // DNSProvider implements the challenge.Provider interface.
  50  type DNSProvider struct {
  51  	config *Config
  52  	client *metaname.MetanameClient
  53  
  54  	records   map[string]string
  55  	recordsMu sync.Mutex
  56  }
  57  
  58  // NewDNSProvider returns a new DNS provider
  59  // using environment variable METANAME_API_KEY for adding and removing the DNS record.
  60  func NewDNSProvider() (*DNSProvider, error) {
  61  	values, err := env.Get(EnvAccountReference, EnvAPIKey)
  62  	if err != nil {
  63  		return nil, fmt.Errorf("metaname: %w", err)
  64  	}
  65  
  66  	config := NewDefaultConfig()
  67  	config.AccountReference = values[EnvAccountReference]
  68  	config.APIKey = values[EnvAPIKey]
  69  
  70  	return NewDNSProviderConfig(config)
  71  }
  72  
  73  // NewDNSProviderConfig return a DNSProvider instance configured for Metaname.
  74  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  75  	if config == nil {
  76  		return nil, errors.New("metaname: the configuration of the DNS provider is nil")
  77  	}
  78  
  79  	if config.AccountReference == "" {
  80  		return nil, errors.New("metaname: missing account reference")
  81  	}
  82  
  83  	if config.APIKey == "" {
  84  		return nil, errors.New("metaname: missing api key")
  85  	}
  86  
  87  	return &DNSProvider{
  88  		config:  config,
  89  		client:  metaname.NewMetanameClient(config.AccountReference, config.APIKey),
  90  		records: make(map[string]string),
  91  	}, nil
  92  }
  93  
  94  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
  95  	info := dns01.GetChallengeInfo(domain, keyAuth)
  96  
  97  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
  98  	if err != nil {
  99  		return fmt.Errorf("metaname: could not find zone for domain %q: %w", domain, err)
 100  	}
 101  
 102  	authZone = dns01.UnFqdn(authZone)
 103  
 104  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
 105  	if err != nil {
 106  		return fmt.Errorf("metaname: could not extract subDomain: %w", err)
 107  	}
 108  
 109  	ctx := context.Background()
 110  
 111  	r := metaname.ResourceRecord{
 112  		Name: subDomain,
 113  		Type: "TXT",
 114  		Aux:  nil,
 115  		Ttl:  d.config.TTL,
 116  		Data: info.Value,
 117  	}
 118  
 119  	ref, err := d.client.CreateDnsRecord(ctx, authZone, r)
 120  	if err != nil {
 121  		return fmt.Errorf("metaname: add record: %w", err)
 122  	}
 123  
 124  	d.recordsMu.Lock()
 125  	d.records[token] = ref
 126  	d.recordsMu.Unlock()
 127  
 128  	return nil
 129  }
 130  
 131  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 132  	info := dns01.GetChallengeInfo(domain, keyAuth)
 133  
 134  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 135  	if err != nil {
 136  		return fmt.Errorf("metaname: could not find zone for domain %q: %w", domain, err)
 137  	}
 138  
 139  	authZone = dns01.UnFqdn(authZone)
 140  
 141  	ctx := context.Background()
 142  
 143  	d.recordsMu.Lock()
 144  	ref, ok := d.records[token]
 145  	d.recordsMu.Unlock()
 146  
 147  	if !ok {
 148  		return fmt.Errorf("metaname: unknown ref for %s", info.EffectiveFQDN)
 149  	}
 150  
 151  	err = d.client.DeleteDnsRecord(ctx, authZone, ref)
 152  	if err != nil {
 153  		return fmt.Errorf("metaname: delete record: %w", err)
 154  	}
 155  
 156  	d.recordsMu.Lock()
 157  	delete(d.records, token)
 158  	d.recordsMu.Unlock()
 159  
 160  	return nil
 161  }
 162  
 163  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 164  // Adjusting here to cope with spikes in propagation times.
 165  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 166  	return d.config.PropagationTimeout, d.config.PollingInterval
 167  }
 168