nicru.go raw

   1  // Package nicru implements a DNS provider for solving the DNS-01 challenge using RU Center.
   2  package nicru
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"strconv"
   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/go-acme/lego/v4/providers/dns/internal/clientdebug"
  15  	"github.com/go-acme/lego/v4/providers/dns/nicru/internal"
  16  )
  17  
  18  // Environment variables names.
  19  const (
  20  	envNamespace = "NICRU_"
  21  
  22  	EnvUsername  = envNamespace + "USER"
  23  	EnvPassword  = envNamespace + "PASSWORD"
  24  	EnvServiceID = envNamespace + "SERVICE_ID"
  25  	EnvSecret    = envNamespace + "SECRET"
  26  
  27  	EnvTTL                = envNamespace + "TTL"
  28  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  29  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  30  )
  31  
  32  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  33  
  34  // Config is used to configure the creation of the DNSProvider.
  35  type Config struct {
  36  	TTL                int
  37  	Username           string
  38  	Password           string
  39  	ServiceID          string
  40  	Secret             string
  41  	PropagationTimeout time.Duration
  42  	PollingInterval    time.Duration
  43  }
  44  
  45  // NewDefaultConfig returns a default configuration for the DNSProvider.
  46  func NewDefaultConfig() *Config {
  47  	return &Config{
  48  		TTL:                env.GetOrDefaultInt(EnvTTL, 30),
  49  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
  50  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 1*time.Minute),
  51  	}
  52  }
  53  
  54  // DNSProvider implements the challenge.Provider interface.
  55  type DNSProvider struct {
  56  	client *internal.Client
  57  	config *Config
  58  }
  59  
  60  // NewDNSProvider returns a DNSProvider instance configured for RU Center.
  61  func NewDNSProvider() (*DNSProvider, error) {
  62  	values, err := env.Get(EnvUsername, EnvPassword, EnvServiceID, EnvSecret)
  63  	if err != nil {
  64  		return nil, fmt.Errorf("nicru: %w", err)
  65  	}
  66  
  67  	config := NewDefaultConfig()
  68  	config.Username = values[EnvUsername]
  69  	config.Password = values[EnvPassword]
  70  	config.ServiceID = values[EnvServiceID]
  71  	config.Secret = values[EnvSecret]
  72  
  73  	return NewDNSProviderConfig(config)
  74  }
  75  
  76  // NewDNSProviderConfig return a DNSProvider instance configured for RU Center.
  77  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  78  	if config == nil {
  79  		return nil, errors.New("nicru: the configuration of the DNS provider is nil")
  80  	}
  81  
  82  	clientCfg := &internal.OauthConfiguration{
  83  		OAuth2ClientID: config.ServiceID,
  84  		OAuth2SecretID: config.Secret,
  85  		Username:       config.Username,
  86  		Password:       config.Password,
  87  	}
  88  
  89  	oauthClient, err := internal.NewOauthClient(context.Background(), clientCfg)
  90  	if err != nil {
  91  		return nil, fmt.Errorf("nicru: %w", err)
  92  	}
  93  
  94  	client, err := internal.NewClient(clientdebug.Wrap(oauthClient))
  95  	if err != nil {
  96  		return nil, fmt.Errorf("nicru: unable to build API client: %w", err)
  97  	}
  98  
  99  	return &DNSProvider{
 100  		client: client,
 101  		config: config,
 102  	}, nil
 103  }
 104  
 105  // Present creates a TXT record to fulfill the dns-01 challenge.
 106  func (d *DNSProvider) Present(domain, _, keyAuth string) error {
 107  	ctx := context.Background()
 108  
 109  	info := dns01.GetChallengeInfo(domain, keyAuth)
 110  
 111  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 112  	if err != nil {
 113  		return fmt.Errorf("nicru: could not find zone for domain %q: %w", domain, err)
 114  	}
 115  
 116  	authZone = dns01.UnFqdn(authZone)
 117  
 118  	zone, err := d.findZone(ctx, authZone)
 119  	if err != nil {
 120  		return fmt.Errorf("nicru: find zone: %w", err)
 121  	}
 122  
 123  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
 124  	if err != nil {
 125  		return fmt.Errorf("nicru: %w", err)
 126  	}
 127  
 128  	records, err := d.client.GetRecords(ctx, zone.Service, authZone)
 129  	if err != nil {
 130  		return fmt.Errorf("nicru: get records: %w", err)
 131  	}
 132  
 133  	for _, record := range records {
 134  		if record.TXT == nil {
 135  			continue
 136  		}
 137  
 138  		if record.TXT.Text == subDomain && record.TXT.String == info.Value {
 139  			return nil
 140  		}
 141  	}
 142  
 143  	rrs := []internal.RR{{
 144  		Name: subDomain,
 145  		TTL:  strconv.Itoa(d.config.TTL),
 146  		Type: "TXT",
 147  		TXT:  &internal.TXT{String: info.Value},
 148  	}}
 149  
 150  	_, err = d.client.AddRecords(ctx, zone.Service, authZone, rrs)
 151  	if err != nil {
 152  		return fmt.Errorf("nicru: add records: %w", err)
 153  	}
 154  
 155  	err = d.client.CommitZone(ctx, zone.Service, authZone)
 156  	if err != nil {
 157  		return fmt.Errorf("nicru: commit zone: %w", err)
 158  	}
 159  
 160  	return nil
 161  }
 162  
 163  // CleanUp removes the TXT record matching the specified parameters.
 164  func (d *DNSProvider) CleanUp(domain, _, keyAuth string) error {
 165  	ctx := context.Background()
 166  
 167  	info := dns01.GetChallengeInfo(domain, keyAuth)
 168  
 169  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 170  	if err != nil {
 171  		return fmt.Errorf("nicru: could not find zone for domain %q: %w", domain, err)
 172  	}
 173  
 174  	authZone = dns01.UnFqdn(authZone)
 175  
 176  	zone, err := d.findZone(ctx, authZone)
 177  	if err != nil {
 178  		return fmt.Errorf("nicru: find zone: %w", err)
 179  	}
 180  
 181  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
 182  	if err != nil {
 183  		return fmt.Errorf("nicru: %w", err)
 184  	}
 185  
 186  	records, err := d.client.GetRecords(ctx, zone.Service, authZone)
 187  	if err != nil {
 188  		return fmt.Errorf("nicru: get records: %w", err)
 189  	}
 190  
 191  	subDomain = dns01.UnFqdn(subDomain)
 192  
 193  	for _, record := range records {
 194  		if record.TXT == nil {
 195  			continue
 196  		}
 197  
 198  		if record.Name != subDomain || record.TXT.String != info.Value {
 199  			continue
 200  		}
 201  
 202  		err = d.client.DeleteRecord(ctx, zone.Service, authZone, record.ID)
 203  		if err != nil {
 204  			return fmt.Errorf("nicru: delete record: %w", err)
 205  		}
 206  	}
 207  
 208  	err = d.client.CommitZone(ctx, zone.Service, authZone)
 209  	if err != nil {
 210  		return fmt.Errorf("nicru: commit zone: %w", err)
 211  	}
 212  
 213  	return nil
 214  }
 215  
 216  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 217  // Adjusting here to cope with spikes in propagation times.
 218  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 219  	return d.config.PropagationTimeout, d.config.PollingInterval
 220  }
 221  
 222  func (d *DNSProvider) findZone(ctx context.Context, authZone string) (*internal.Zone, error) {
 223  	zones, err := d.client.ListZones(ctx)
 224  	if err != nil {
 225  		return nil, fmt.Errorf("unable to fetch dns zones: %w", err)
 226  	}
 227  
 228  	if len(zones) == 0 {
 229  		return nil, errors.New("no zones found")
 230  	}
 231  
 232  	for _, zone := range zones {
 233  		if zone.Name == authZone {
 234  			return &zone, nil
 235  		}
 236  	}
 237  
 238  	return nil, fmt.Errorf("zone not found for %s", authZone)
 239  }
 240