wedos.go raw

   1  package wedos
   2  
   3  import (
   4  	"context"
   5  	"encoding/json"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"strconv"
  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/wedos/internal"
  17  )
  18  
  19  // Environment variables names.
  20  const (
  21  	envNamespace = "WEDOS_"
  22  
  23  	EnvUsername = envNamespace + "USERNAME"
  24  	EnvPassword = envNamespace + "WAPI_PASSWORD"
  25  
  26  	EnvTTL                = envNamespace + "TTL"
  27  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  28  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  29  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  30  )
  31  
  32  const minTTL = 5 * 60 // 5 minutes
  33  
  34  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  35  
  36  // Config is used to configure the creation of the DNSProvider.
  37  type Config struct {
  38  	Username           string
  39  	Password           string
  40  	PropagationTimeout time.Duration
  41  	PollingInterval    time.Duration
  42  	TTL                int
  43  	HTTPClient         *http.Client
  44  }
  45  
  46  // NewDefaultConfig returns a default configuration for the DNSProvider.
  47  func NewDefaultConfig() *Config {
  48  	return &Config{
  49  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 10*time.Minute),
  50  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
  51  		TTL:                env.GetOrDefaultInt(EnvTTL, minTTL),
  52  		HTTPClient: &http.Client{
  53  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
  54  		},
  55  	}
  56  }
  57  
  58  // DNSProvider implements the challenge.Provider interface.
  59  type DNSProvider struct {
  60  	config *Config
  61  	client *internal.Client
  62  }
  63  
  64  // NewDNSProvider returns a DNSProvider instance.
  65  func NewDNSProvider() (*DNSProvider, error) {
  66  	values, err := env.Get(EnvUsername, EnvPassword)
  67  	if err != nil {
  68  		return nil, fmt.Errorf("wedos: %w", err)
  69  	}
  70  
  71  	config := NewDefaultConfig()
  72  	config.Username = values[EnvUsername]
  73  	config.Password = values[EnvPassword]
  74  
  75  	return NewDNSProviderConfig(config)
  76  }
  77  
  78  // NewDNSProviderConfig return a DNSProvider.
  79  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  80  	if config == nil {
  81  		return nil, errors.New("wedos: the configuration of the DNS provider is nil")
  82  	}
  83  
  84  	if config.Username == "" || config.Password == "" {
  85  		return nil, errors.New("wedos: some credentials information are missing")
  86  	}
  87  
  88  	if config.TTL < minTTL {
  89  		return nil, fmt.Errorf("wedos: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
  90  	}
  91  
  92  	client := internal.NewClient(config.Username, config.Password)
  93  
  94  	if config.HTTPClient != nil {
  95  		client.HTTPClient = config.HTTPClient
  96  	}
  97  
  98  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
  99  
 100  	return &DNSProvider{config: config, client: client}, nil
 101  }
 102  
 103  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 104  // Adjusting here to cope with spikes in propagation times.
 105  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 106  	return d.config.PropagationTimeout, d.config.PollingInterval
 107  }
 108  
 109  // Present creates a TXT record to fulfill the dns-01 challenge.
 110  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 111  	ctx := context.Background()
 112  
 113  	info := dns01.GetChallengeInfo(domain, keyAuth)
 114  
 115  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 116  	if err != nil {
 117  		return fmt.Errorf("wedos: could not find zone for domain %q: %w", domain, err)
 118  	}
 119  
 120  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
 121  	if err != nil {
 122  		return fmt.Errorf("wedos: %w", err)
 123  	}
 124  
 125  	record := internal.DNSRow{
 126  		Name: subDomain,
 127  		TTL:  json.Number(strconv.Itoa(d.config.TTL)),
 128  		Type: "TXT",
 129  		Data: info.Value,
 130  	}
 131  
 132  	records, err := d.client.GetRecords(ctx, authZone)
 133  	if err != nil {
 134  		return fmt.Errorf("wedos: could not get records for domain %q: %w", domain, err)
 135  	}
 136  
 137  	for _, candidate := range records {
 138  		if candidate.Type == "TXT" && candidate.Name == subDomain && candidate.Data == info.Value {
 139  			record.ID = candidate.ID
 140  			break
 141  		}
 142  	}
 143  
 144  	err = d.client.AddRecord(ctx, authZone, record)
 145  	if err != nil {
 146  		return fmt.Errorf("wedos: could not add TXT record for domain %q: %w", domain, err)
 147  	}
 148  
 149  	err = d.client.Commit(ctx, authZone)
 150  	if err != nil {
 151  		return fmt.Errorf("wedos: could not commit TXT record for domain %q: %w", domain, err)
 152  	}
 153  
 154  	return nil
 155  }
 156  
 157  // CleanUp removes the TXT record matching the specified parameters.
 158  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 159  	ctx := context.Background()
 160  
 161  	info := dns01.GetChallengeInfo(domain, keyAuth)
 162  
 163  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 164  	if err != nil {
 165  		return fmt.Errorf("wedos: could not find zone for domain %q: %w", domain, err)
 166  	}
 167  
 168  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
 169  	if err != nil {
 170  		return fmt.Errorf("wedos: %w", err)
 171  	}
 172  
 173  	records, err := d.client.GetRecords(ctx, authZone)
 174  	if err != nil {
 175  		return fmt.Errorf("wedos: could not get records for domain %q: %w", domain, err)
 176  	}
 177  
 178  	for _, candidate := range records {
 179  		if candidate.Type != "TXT" || candidate.Name != subDomain || candidate.Data != info.Value {
 180  			continue
 181  		}
 182  
 183  		err = d.client.DeleteRecord(ctx, authZone, candidate.ID)
 184  		if err != nil {
 185  			return fmt.Errorf("wedos: could not remove TXT record for domain %q: %w", domain, err)
 186  		}
 187  
 188  		err = d.client.Commit(ctx, authZone)
 189  		if err != nil {
 190  			return fmt.Errorf("wedos: could not commit TXT record for domain %q: %w", domain, err)
 191  		}
 192  
 193  		return nil
 194  	}
 195  
 196  	return nil
 197  }
 198