provider.go raw

   1  // Package hostingde implements a DNS provider for solving the DNS-01 challenge using hosting.de.
   2  package hostingde
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"net/url"
  10  	"sync"
  11  	"time"
  12  
  13  	"github.com/go-acme/lego/v4/challenge"
  14  	"github.com/go-acme/lego/v4/challenge/dns01"
  15  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  16  	"github.com/go-acme/lego/v4/providers/dns/internal/hostingde/internal"
  17  )
  18  
  19  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  20  
  21  // Config is used to configure the creation of the DNSProvider.
  22  type Config struct {
  23  	APIKey             string
  24  	ZoneName           string
  25  	PropagationTimeout time.Duration
  26  	PollingInterval    time.Duration
  27  	TTL                int
  28  	HTTPClient         *http.Client
  29  }
  30  
  31  // DNSProvider implements the challenge.Provider interface.
  32  type DNSProvider struct {
  33  	config *Config
  34  	client *internal.Client
  35  
  36  	recordIDs   map[string]string
  37  	recordIDsMu sync.Mutex
  38  }
  39  
  40  // NewDNSProviderConfig return a DNSProvider instance configured for hosting.de.
  41  func NewDNSProviderConfig(config *Config, baseURL string) (*DNSProvider, error) {
  42  	if config == nil {
  43  		return nil, errors.New("the configuration of the DNS provider is nil")
  44  	}
  45  
  46  	if config.APIKey == "" {
  47  		return nil, errors.New("API key missing")
  48  	}
  49  
  50  	client := internal.NewClient(config.APIKey)
  51  
  52  	if baseURL != "" {
  53  		client.BaseURL, _ = url.Parse(baseURL)
  54  	}
  55  
  56  	if config.HTTPClient != nil {
  57  		client.HTTPClient = config.HTTPClient
  58  	}
  59  
  60  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
  61  
  62  	return &DNSProvider{
  63  		config:    config,
  64  		client:    client,
  65  		recordIDs: make(map[string]string),
  66  	}, nil
  67  }
  68  
  69  // Timeout returns the timeout and interval to use when checking for DNS propagation.
  70  // Adjusting here to cope with spikes in propagation times.
  71  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
  72  	return d.config.PropagationTimeout, d.config.PollingInterval
  73  }
  74  
  75  // Present creates a TXT record to fulfill the dns-01 challenge.
  76  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
  77  	info := dns01.GetChallengeInfo(domain, keyAuth)
  78  
  79  	zoneName, err := d.getZoneName(info.EffectiveFQDN)
  80  	if err != nil {
  81  		return fmt.Errorf("could not find zone for domain %q: %w", domain, err)
  82  	}
  83  
  84  	ctx := context.Background()
  85  
  86  	// get the ZoneConfig for that domain
  87  	zonesFind := internal.ZoneConfigsFindRequest{
  88  		Filter: internal.Filter{Field: "zoneName", Value: zoneName},
  89  		Limit:  1,
  90  		Page:   1,
  91  	}
  92  
  93  	zoneConfig, err := d.client.GetZone(ctx, zonesFind)
  94  	if err != nil {
  95  		return err
  96  	}
  97  
  98  	zoneConfig.Name = zoneName
  99  
 100  	rec := []internal.DNSRecord{{
 101  		Type:    "TXT",
 102  		Name:    dns01.UnFqdn(info.EffectiveFQDN),
 103  		Content: info.Value,
 104  		TTL:     d.config.TTL,
 105  	}}
 106  
 107  	req := internal.ZoneUpdateRequest{
 108  		ZoneConfig:   *zoneConfig,
 109  		RecordsToAdd: rec,
 110  	}
 111  
 112  	response, err := d.client.UpdateZone(ctx, req)
 113  	if err != nil {
 114  		return err
 115  	}
 116  
 117  	for _, record := range response.Records {
 118  		if record.Name == dns01.UnFqdn(info.EffectiveFQDN) && record.Content == fmt.Sprintf(`%q`, info.Value) {
 119  			d.recordIDsMu.Lock()
 120  			d.recordIDs[info.EffectiveFQDN] = record.ID
 121  			d.recordIDsMu.Unlock()
 122  		}
 123  	}
 124  
 125  	if d.recordIDs[info.EffectiveFQDN] == "" {
 126  		return fmt.Errorf("error getting ID of just created record, for domain %s", domain)
 127  	}
 128  
 129  	return nil
 130  }
 131  
 132  // CleanUp removes the TXT record matching the specified parameters.
 133  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 134  	info := dns01.GetChallengeInfo(domain, keyAuth)
 135  
 136  	zoneName, err := d.getZoneName(info.EffectiveFQDN)
 137  	if err != nil {
 138  		return fmt.Errorf("could not find zone for domain %q: %w", domain, err)
 139  	}
 140  
 141  	ctx := context.Background()
 142  
 143  	// get the ZoneConfig for that domain
 144  	zonesFind := internal.ZoneConfigsFindRequest{
 145  		Filter: internal.Filter{Field: "zoneName", Value: zoneName},
 146  		Limit:  1,
 147  		Page:   1,
 148  	}
 149  
 150  	zoneConfig, err := d.client.GetZone(ctx, zonesFind)
 151  	if err != nil {
 152  		return err
 153  	}
 154  
 155  	zoneConfig.Name = zoneName
 156  
 157  	rec := []internal.DNSRecord{{
 158  		Type:    "TXT",
 159  		Name:    dns01.UnFqdn(info.EffectiveFQDN),
 160  		Content: `"` + info.Value + `"`,
 161  	}}
 162  
 163  	req := internal.ZoneUpdateRequest{
 164  		ZoneConfig:      *zoneConfig,
 165  		RecordsToDelete: rec,
 166  	}
 167  
 168  	_, err = d.client.UpdateZone(ctx, req)
 169  	if err != nil {
 170  		return err
 171  	}
 172  
 173  	// Delete record ID from map
 174  	d.recordIDsMu.Lock()
 175  	delete(d.recordIDs, info.EffectiveFQDN)
 176  	d.recordIDsMu.Unlock()
 177  
 178  	return nil
 179  }
 180  
 181  func (d *DNSProvider) getZoneName(fqdn string) (string, error) {
 182  	if d.config.ZoneName != "" {
 183  		return d.config.ZoneName, nil
 184  	}
 185  
 186  	zoneName, err := dns01.FindZoneByFqdn(fqdn)
 187  	if err != nil {
 188  		return "", fmt.Errorf("could not find zone for %s: %w", fqdn, err)
 189  	}
 190  
 191  	if zoneName == "" {
 192  		return "", errors.New("empty zone name")
 193  	}
 194  
 195  	return dns01.UnFqdn(zoneName), nil
 196  }
 197