nifcloud.go raw

   1  // Package nifcloud implements a DNS provider for solving the DNS-01 challenge using NIFCLOUD DNS.
   2  package nifcloud
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"net/url"
  10  	"time"
  11  
  12  	"github.com/cenkalti/backoff/v5"
  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/platform/wait"
  17  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  18  	"github.com/go-acme/lego/v4/providers/dns/nifcloud/internal"
  19  )
  20  
  21  // Environment variables names.
  22  const (
  23  	envNamespace = "NIFCLOUD_"
  24  
  25  	EnvAccessKeyID     = envNamespace + "ACCESS_KEY_ID"
  26  	EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY"
  27  	EnvDNSEndpoint     = envNamespace + "DNS_ENDPOINT"
  28  
  29  	EnvTTL                = envNamespace + "TTL"
  30  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  31  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  32  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  33  )
  34  
  35  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  36  
  37  // Config is used to configure the creation of the DNSProvider.
  38  type Config struct {
  39  	BaseURL            string
  40  	AccessKey          string
  41  	SecretKey          string
  42  	PropagationTimeout time.Duration
  43  	PollingInterval    time.Duration
  44  	TTL                int
  45  	HTTPClient         *http.Client
  46  }
  47  
  48  // NewDefaultConfig returns a default configuration for the DNSProvider.
  49  func NewDefaultConfig() *Config {
  50  	return &Config{
  51  		TTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
  52  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  53  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  54  		HTTPClient: &http.Client{
  55  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
  56  		},
  57  	}
  58  }
  59  
  60  // DNSProvider implements the challenge.Provider interface.
  61  type DNSProvider struct {
  62  	client *internal.Client
  63  	config *Config
  64  }
  65  
  66  // NewDNSProvider returns a DNSProvider instance configured for the NIFCLOUD DNS service.
  67  // Credentials must be passed in the environment variables:
  68  // NIFCLOUD_ACCESS_KEY_ID and NIFCLOUD_SECRET_ACCESS_KEY.
  69  func NewDNSProvider() (*DNSProvider, error) {
  70  	values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey)
  71  	if err != nil {
  72  		return nil, fmt.Errorf("nifcloud: %w", err)
  73  	}
  74  
  75  	config := NewDefaultConfig()
  76  	config.BaseURL = env.GetOrFile(EnvDNSEndpoint)
  77  	config.AccessKey = values[EnvAccessKeyID]
  78  	config.SecretKey = values[EnvSecretAccessKey]
  79  
  80  	return NewDNSProviderConfig(config)
  81  }
  82  
  83  // NewDNSProviderConfig return a DNSProvider instance configured for NIFCLOUD.
  84  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  85  	if config == nil {
  86  		return nil, errors.New("nifcloud: the configuration of the DNS provider is nil")
  87  	}
  88  
  89  	client, err := internal.NewClient(config.AccessKey, config.SecretKey)
  90  	if err != nil {
  91  		return nil, fmt.Errorf("nifcloud: %w", err)
  92  	}
  93  
  94  	if config.HTTPClient != nil {
  95  		client.HTTPClient = config.HTTPClient
  96  	}
  97  
  98  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
  99  
 100  	if config.BaseURL != "" {
 101  		baseURL, err := url.Parse(config.BaseURL)
 102  		if err != nil {
 103  			return nil, fmt.Errorf("nifcloud: %w", err)
 104  		}
 105  
 106  		client.BaseURL = baseURL
 107  	}
 108  
 109  	return &DNSProvider{client: client, config: config}, nil
 110  }
 111  
 112  // Present creates a TXT record using the specified parameters.
 113  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 114  	ctx := context.Background()
 115  
 116  	info := dns01.GetChallengeInfo(domain, keyAuth)
 117  
 118  	err := d.changeRecord(ctx, "CREATE", info.EffectiveFQDN, info.Value, d.config.TTL)
 119  	if err != nil {
 120  		return fmt.Errorf("nifcloud: %w", err)
 121  	}
 122  
 123  	return err
 124  }
 125  
 126  // CleanUp removes the TXT record matching the specified parameters.
 127  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 128  	ctx := context.Background()
 129  
 130  	info := dns01.GetChallengeInfo(domain, keyAuth)
 131  
 132  	err := d.changeRecord(ctx, "DELETE", info.EffectiveFQDN, info.Value, d.config.TTL)
 133  	if err != nil {
 134  		return fmt.Errorf("nifcloud: %w", err)
 135  	}
 136  
 137  	return err
 138  }
 139  
 140  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 141  // Adjusting here to cope with spikes in propagation times.
 142  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 143  	return d.config.PropagationTimeout, d.config.PollingInterval
 144  }
 145  
 146  func (d *DNSProvider) changeRecord(ctx context.Context, action, fqdn, value string, ttl int) error {
 147  	authZone, err := dns01.FindZoneByFqdn(fqdn)
 148  	if err != nil {
 149  		return fmt.Errorf("could not find zone: %w", err)
 150  	}
 151  
 152  	name := dns01.UnFqdn(fqdn)
 153  	if authZone == fqdn {
 154  		name = "@"
 155  	}
 156  
 157  	reqParams := internal.ChangeResourceRecordSetsRequest{
 158  		XMLNs: internal.XMLNs,
 159  		ChangeBatch: internal.ChangeBatch{
 160  			Comment: "Managed by Lego",
 161  			Changes: internal.Changes{
 162  				Change: []internal.Change{
 163  					{
 164  						Action: action,
 165  						ResourceRecordSet: internal.ResourceRecordSet{
 166  							Name: name,
 167  							Type: "TXT",
 168  							TTL:  ttl,
 169  							ResourceRecords: internal.ResourceRecords{
 170  								ResourceRecord: []internal.ResourceRecord{
 171  									{
 172  										Value: value,
 173  									},
 174  								},
 175  							},
 176  						},
 177  					},
 178  				},
 179  			},
 180  		},
 181  	}
 182  
 183  	resp, err := d.client.ChangeResourceRecordSets(ctx, dns01.UnFqdn(authZone), reqParams)
 184  	if err != nil {
 185  		return fmt.Errorf("failed to change record set: %w", err)
 186  	}
 187  
 188  	statusID := resp.ChangeInfo.ID
 189  
 190  	return wait.Retry(ctx,
 191  		func() error {
 192  			resp, err := d.client.GetChange(ctx, statusID)
 193  			if err != nil {
 194  				return fmt.Errorf("get change: %w", err)
 195  			}
 196  
 197  			if resp.ChangeInfo.Status != "INSYNC" {
 198  				return fmt.Errorf("change status: %s", resp.ChangeInfo.Status)
 199  			}
 200  
 201  			return nil
 202  		},
 203  		backoff.WithBackOff(backoff.NewConstantBackOff(4*time.Second)),
 204  		backoff.WithMaxElapsedTime(120*time.Second),
 205  	)
 206  }
 207