nicmanager.go raw

   1  // Package nicmanager implements a DNS provider for solving the DNS-01 challenge using nicmanager DNS.
   2  package nicmanager
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"strings"
  10  	"time"
  11  
  12  	"github.com/go-acme/lego/v4/challenge"
  13  	"github.com/go-acme/lego/v4/challenge/dns01"
  14  	"github.com/go-acme/lego/v4/platform/config/env"
  15  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  16  	"github.com/go-acme/lego/v4/providers/dns/nicmanager/internal"
  17  )
  18  
  19  // Environment variables names.
  20  const (
  21  	envNamespace = "NICMANAGER_"
  22  
  23  	EnvLogin    = envNamespace + "API_LOGIN"
  24  	EnvUsername = envNamespace + "API_USERNAME"
  25  	EnvEmail    = envNamespace + "API_EMAIL"
  26  	EnvPassword = envNamespace + "API_PASSWORD"
  27  	EnvOTP      = envNamespace + "API_OTP"
  28  	EnvMode     = envNamespace + "API_MODE"
  29  
  30  	EnvTTL                = envNamespace + "TTL"
  31  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  32  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  33  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  34  )
  35  
  36  const minTTL = 900
  37  
  38  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  39  
  40  // Config is used to configure the creation of the DNSProvider.
  41  type Config struct {
  42  	Login     string
  43  	Username  string
  44  	Email     string
  45  	Password  string
  46  	OTPSecret string
  47  	Mode      string
  48  
  49  	PropagationTimeout time.Duration
  50  	PollingInterval    time.Duration
  51  	TTL                int
  52  	HTTPClient         *http.Client
  53  }
  54  
  55  // NewDefaultConfig returns a default configuration for the DNSProvider.
  56  func NewDefaultConfig() *Config {
  57  	return &Config{
  58  		TTL:                env.GetOrDefaultInt(EnvTTL, minTTL),
  59  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 5*time.Minute),
  60  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  61  		HTTPClient: &http.Client{
  62  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
  63  		},
  64  	}
  65  }
  66  
  67  // DNSProvider implements the challenge.Provider interface.
  68  type DNSProvider struct {
  69  	client *internal.Client
  70  	config *Config
  71  }
  72  
  73  // NewDNSProvider returns a DNSProvider instance configured for nicmanager.
  74  // Credentials must be passed in the environment variables:
  75  // NICMANAGER_API_LOGIN, NICMANAGER_API_USERNAME
  76  // NICMANAGER_API_EMAIL
  77  // NICMANAGER_API_PASSWORD
  78  // NICMANAGER_API_OTP
  79  // NICMANAGER_API_MODE.
  80  func NewDNSProvider() (*DNSProvider, error) {
  81  	values, err := env.Get(EnvPassword)
  82  	if err != nil {
  83  		return nil, fmt.Errorf("nicmanager: %w", err)
  84  	}
  85  
  86  	config := NewDefaultConfig()
  87  	config.Password = values[EnvPassword]
  88  
  89  	config.Mode = env.GetOneWithFallback(EnvMode, internal.ModeAnycast, env.ParseString, envNamespace+"MODE")
  90  	config.Username = env.GetOrFile(EnvUsername)
  91  	config.Login = env.GetOrFile(EnvLogin)
  92  	config.Email = env.GetOrFile(EnvEmail)
  93  	config.OTPSecret = env.GetOrFile(EnvOTP)
  94  
  95  	if config.TTL < minTTL {
  96  		return nil, fmt.Errorf("TTL must be higher than %d: %d", minTTL, config.TTL)
  97  	}
  98  
  99  	return NewDNSProviderConfig(config)
 100  }
 101  
 102  // NewDNSProviderConfig return a DNSProvider instance configured for nicmanager.
 103  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
 104  	if config == nil {
 105  		return nil, errors.New("nicmanager: the configuration of the DNS provider is nil")
 106  	}
 107  
 108  	opts := internal.Options{
 109  		Password: config.Password,
 110  		OTP:      config.OTPSecret,
 111  		Mode:     config.Mode,
 112  	}
 113  
 114  	switch {
 115  	case config.Password == "":
 116  		return nil, errors.New("nicmanager: credentials missing")
 117  	case config.Email != "":
 118  		opts.Email = config.Email
 119  	case config.Login != "" && config.Username != "":
 120  		opts.Login = config.Login
 121  		opts.Username = config.Username
 122  	default:
 123  		return nil, errors.New("nicmanager: credentials missing")
 124  	}
 125  
 126  	client := internal.NewClient(opts)
 127  
 128  	if config.HTTPClient != nil {
 129  		client.HTTPClient = config.HTTPClient
 130  	}
 131  
 132  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
 133  
 134  	return &DNSProvider{client: client, config: config}, nil
 135  }
 136  
 137  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 138  // Adjusting here to cope with spikes in propagation times.
 139  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 140  	return d.config.PropagationTimeout, d.config.PollingInterval
 141  }
 142  
 143  // Present creates a TXT record to fulfill the dns-01 challenge.
 144  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 145  	info := dns01.GetChallengeInfo(domain, keyAuth)
 146  
 147  	rootDomain, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 148  	if err != nil {
 149  		return fmt.Errorf("nicmanager: could not find zone for domain %q: %w", domain, err)
 150  	}
 151  
 152  	ctx := context.Background()
 153  
 154  	zone, err := d.client.GetZone(ctx, dns01.UnFqdn(rootDomain))
 155  	if err != nil {
 156  		return fmt.Errorf("nicmanager: failed to get zone %q: %w", rootDomain, err)
 157  	}
 158  
 159  	// The way nic manager deals with record with multiple values is that they are completely different records with unique ids
 160  	// Hence we don't check for an existing record here, but rather just create one
 161  	record := internal.RecordCreateUpdate{
 162  		Name:  info.EffectiveFQDN,
 163  		Type:  "TXT",
 164  		TTL:   d.config.TTL,
 165  		Value: info.Value,
 166  	}
 167  
 168  	err = d.client.AddRecord(ctx, zone.Name, record)
 169  	if err != nil {
 170  		return fmt.Errorf("nicmanager: failed to create record [zone: %q, fqdn: %q]: %w", zone.Name, info.EffectiveFQDN, err)
 171  	}
 172  
 173  	return nil
 174  }
 175  
 176  // CleanUp removes the TXT record matching the specified parameters.
 177  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 178  	info := dns01.GetChallengeInfo(domain, keyAuth)
 179  
 180  	rootDomain, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 181  	if err != nil {
 182  		return fmt.Errorf("nicmanager: could not find zone for domain %q: %w", domain, err)
 183  	}
 184  
 185  	ctx := context.Background()
 186  
 187  	zone, err := d.client.GetZone(ctx, dns01.UnFqdn(rootDomain))
 188  	if err != nil {
 189  		return fmt.Errorf("nicmanager: failed to get zone %q: %w", rootDomain, err)
 190  	}
 191  
 192  	name := dns01.UnFqdn(info.EffectiveFQDN)
 193  
 194  	var (
 195  		existingRecord      internal.Record
 196  		existingRecordFound bool
 197  	)
 198  
 199  	for _, record := range zone.Records {
 200  		if strings.EqualFold(record.Type, "TXT") && strings.EqualFold(record.Name, name) && record.Content == info.Value {
 201  			existingRecord = record
 202  			existingRecordFound = true
 203  		}
 204  	}
 205  
 206  	if existingRecordFound {
 207  		err = d.client.DeleteRecord(ctx, zone.Name, existingRecord.ID)
 208  		if err != nil {
 209  			return fmt.Errorf("nicmanager: failed to delete record [zone: %q, domain: %q]: %w", zone.Name, name, err)
 210  		}
 211  	}
 212  
 213  	return errors.New("nicmanager: no record found to clean up")
 214  }
 215