provider.go raw

   1  // Package active24 implements a DNS provider for solving the DNS-01 challenge using Active24.
   2  package active24
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"strconv"
  10  	"time"
  11  
  12  	"github.com/go-acme/lego/v4/challenge/dns01"
  13  	"github.com/go-acme/lego/v4/providers/dns/internal/active24/internal"
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  15  )
  16  
  17  // Config is used to configure the creation of the DNSProvider.
  18  type Config struct {
  19  	APIKey string
  20  	Secret string
  21  
  22  	PropagationTimeout time.Duration
  23  	PollingInterval    time.Duration
  24  	TTL                int
  25  	HTTPClient         *http.Client
  26  }
  27  
  28  // DNSProvider implements the challenge.Provider interface.
  29  type DNSProvider struct {
  30  	config *Config
  31  	client *internal.Client
  32  }
  33  
  34  // NewDNSProviderConfig return a DNSProvider instance configured for Active24.
  35  func NewDNSProviderConfig(config *Config, baseAPIDomain string) (*DNSProvider, error) {
  36  	if config == nil {
  37  		return nil, errors.New("the configuration of the DNS provider is nil")
  38  	}
  39  
  40  	client, err := internal.NewClient(baseAPIDomain, config.APIKey, config.Secret)
  41  	if err != nil {
  42  		return nil, err
  43  	}
  44  
  45  	if config.HTTPClient != nil {
  46  		client.HTTPClient = config.HTTPClient
  47  	}
  48  
  49  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
  50  
  51  	return &DNSProvider{
  52  		config: config,
  53  		client: client,
  54  	}, nil
  55  }
  56  
  57  // Present creates a TXT record using the specified parameters.
  58  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
  59  	ctx := context.Background()
  60  
  61  	info := dns01.GetChallengeInfo(domain, keyAuth)
  62  
  63  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
  64  	if err != nil {
  65  		return fmt.Errorf("could not find zone for domain %q: %w", domain, err)
  66  	}
  67  
  68  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
  69  	if err != nil {
  70  		return err
  71  	}
  72  
  73  	serviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone))
  74  	if err != nil {
  75  		return fmt.Errorf("find service ID: %w", err)
  76  	}
  77  
  78  	record := internal.Record{
  79  		Type:    "TXT",
  80  		Name:    subDomain,
  81  		Content: info.Value,
  82  		TTL:     d.config.TTL,
  83  	}
  84  
  85  	err = d.client.CreateRecord(ctx, strconv.Itoa(serviceID), record)
  86  	if err != nil {
  87  		return fmt.Errorf("create record: %w", err)
  88  	}
  89  
  90  	return nil
  91  }
  92  
  93  // CleanUp removes the TXT record matching the specified parameters.
  94  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
  95  	ctx := context.Background()
  96  
  97  	info := dns01.GetChallengeInfo(domain, keyAuth)
  98  
  99  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 100  	if err != nil {
 101  		return fmt.Errorf("could not find zone for domain %q: %w", domain, err)
 102  	}
 103  
 104  	serviceID, err := d.findServiceID(ctx, dns01.UnFqdn(authZone))
 105  	if err != nil {
 106  		return fmt.Errorf("find service ID: %w", err)
 107  	}
 108  
 109  	recordID, err := d.findRecordID(ctx, strconv.Itoa(serviceID), info)
 110  	if err != nil {
 111  		return fmt.Errorf("find record ID: %w", err)
 112  	}
 113  
 114  	err = d.client.DeleteRecord(ctx, strconv.Itoa(serviceID), strconv.Itoa(recordID))
 115  	if err != nil {
 116  		return fmt.Errorf("delete record %w", err)
 117  	}
 118  
 119  	return nil
 120  }
 121  
 122  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 123  // Adjusting here to cope with spikes in propagation times.
 124  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 125  	return d.config.PropagationTimeout, d.config.PollingInterval
 126  }
 127  
 128  func (d *DNSProvider) findServiceID(ctx context.Context, domain string) (int, error) {
 129  	services, err := d.client.GetServices(ctx)
 130  	if err != nil {
 131  		return 0, fmt.Errorf("get services: %w", err)
 132  	}
 133  
 134  	for _, service := range services {
 135  		if service.ServiceName != "domain" {
 136  			continue
 137  		}
 138  
 139  		if service.Name != domain {
 140  			continue
 141  		}
 142  
 143  		return service.ID, nil
 144  	}
 145  
 146  	return 0, fmt.Errorf("service not found for domain: %s", domain)
 147  }
 148  
 149  func (d *DNSProvider) findRecordID(ctx context.Context, serviceID string, info dns01.ChallengeInfo) (int, error) {
 150  	// NOTE(ldez): Despite the API documentation, the filter doesn't seem to work.
 151  	filter := internal.RecordFilter{
 152  		Name:    dns01.UnFqdn(info.EffectiveFQDN),
 153  		Type:    []string{"TXT"},
 154  		Content: info.Value,
 155  	}
 156  
 157  	records, err := d.client.GetRecords(ctx, serviceID, filter)
 158  	if err != nil {
 159  		return 0, fmt.Errorf("get records: %w", err)
 160  	}
 161  
 162  	for _, record := range records {
 163  		if record.Type != "TXT" {
 164  			continue
 165  		}
 166  
 167  		if record.Name != dns01.UnFqdn(info.EffectiveFQDN) {
 168  			continue
 169  		}
 170  
 171  		if record.Content != info.Value {
 172  			continue
 173  		}
 174  
 175  		return record.ID, nil
 176  	}
 177  
 178  	return 0, errors.New("no record found")
 179  }
 180