provider_dmapi.go raw

   1  package joker
   2  
   3  import (
   4  	"context"
   5  	"errors"
   6  	"fmt"
   7  	"time"
   8  
   9  	"github.com/go-acme/lego/v4/challenge"
  10  	"github.com/go-acme/lego/v4/challenge/dns01"
  11  	"github.com/go-acme/lego/v4/log"
  12  	"github.com/go-acme/lego/v4/platform/config/env"
  13  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  14  	"github.com/go-acme/lego/v4/providers/dns/joker/internal/dmapi"
  15  )
  16  
  17  var _ challenge.ProviderTimeout = (*dmapiProvider)(nil)
  18  
  19  // dmapiProvider implements the challenge.Provider interface.
  20  type dmapiProvider struct {
  21  	config *Config
  22  	client *dmapi.Client
  23  }
  24  
  25  // newDmapiProvider returns a DNSProvider instance configured for Joker.
  26  // Credentials must be passed in the environment variable: JOKER_USERNAME, JOKER_PASSWORD or JOKER_API_KEY.
  27  func newDmapiProvider() (*dmapiProvider, error) {
  28  	values, err := env.Get(EnvAPIKey)
  29  	if err != nil {
  30  		var errU error
  31  
  32  		values, errU = env.Get(EnvUsername, EnvPassword)
  33  		if errU != nil {
  34  			//nolint:errorlint // false-positive
  35  			return nil, fmt.Errorf("joker: %v or %v", errU, err)
  36  		}
  37  	}
  38  
  39  	config := NewDefaultConfig()
  40  	config.APIKey = values[EnvAPIKey]
  41  	config.Username = values[EnvUsername]
  42  	config.Password = values[EnvPassword]
  43  
  44  	return newDmapiProviderConfig(config)
  45  }
  46  
  47  // newDmapiProviderConfig return a DNSProvider instance configured for Joker.
  48  func newDmapiProviderConfig(config *Config) (*dmapiProvider, error) {
  49  	if config == nil {
  50  		return nil, errors.New("joker: the configuration of the DNS provider is nil")
  51  	}
  52  
  53  	if config.APIKey == "" {
  54  		if config.Username == "" || config.Password == "" {
  55  			return nil, errors.New("joker: credentials missing")
  56  		}
  57  	}
  58  
  59  	client := dmapi.NewClient(dmapi.AuthInfo{
  60  		APIKey:   config.APIKey,
  61  		Username: config.Username,
  62  		Password: config.Password,
  63  	})
  64  
  65  	client.Debug = config.Debug
  66  
  67  	if config.HTTPClient != nil {
  68  		client.HTTPClient = config.HTTPClient
  69  	}
  70  
  71  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
  72  
  73  	return &dmapiProvider{config: config, client: client}, nil
  74  }
  75  
  76  // Timeout returns the timeout and interval to use when checking for DNS propagation.
  77  // Adjusting here to cope with spikes in propagation times.
  78  func (d *dmapiProvider) Timeout() (timeout, interval time.Duration) {
  79  	return d.config.PropagationTimeout, d.config.PollingInterval
  80  }
  81  
  82  // Present creates a TXT record using the specified parameters.
  83  func (d *dmapiProvider) Present(domain, token, keyAuth string) error {
  84  	info := dns01.GetChallengeInfo(domain, keyAuth)
  85  
  86  	zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
  87  	if err != nil {
  88  		return fmt.Errorf("joker: could not find zone for domain %q: %w", domain, err)
  89  	}
  90  
  91  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)
  92  	if err != nil {
  93  		return fmt.Errorf("joker: %w", err)
  94  	}
  95  
  96  	if d.config.Debug {
  97  		log.Infof("[%s] joker: adding TXT record %q to zone %q with value %q", domain, subDomain, zone, info.Value)
  98  	}
  99  
 100  	ctx, err := d.client.CreateAuthenticatedContext(context.Background())
 101  	if err != nil {
 102  		return err
 103  	}
 104  
 105  	response, err := d.client.GetZone(ctx, zone)
 106  	if err != nil || response.StatusCode != 0 {
 107  		return formatResponseError(response, err)
 108  	}
 109  
 110  	dnsZone := dmapi.AddTxtEntryToZone(response.Body, subDomain, info.Value, d.config.TTL)
 111  
 112  	response, err = d.client.PutZone(ctx, zone, dnsZone)
 113  	if err != nil || response.StatusCode != 0 {
 114  		return formatResponseError(response, err)
 115  	}
 116  
 117  	return nil
 118  }
 119  
 120  // CleanUp removes the TXT record matching the specified parameters.
 121  func (d *dmapiProvider) CleanUp(domain, token, keyAuth string) error {
 122  	info := dns01.GetChallengeInfo(domain, keyAuth)
 123  
 124  	zone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 125  	if err != nil {
 126  		return fmt.Errorf("joker: could not find zone for domain %q: %w", domain, err)
 127  	}
 128  
 129  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone)
 130  	if err != nil {
 131  		return fmt.Errorf("joker: %w", err)
 132  	}
 133  
 134  	if d.config.Debug {
 135  		log.Infof("[%s] joker: removing entry %q from zone %q", domain, subDomain, zone)
 136  	}
 137  
 138  	ctx, err := d.client.CreateAuthenticatedContext(context.Background())
 139  	if err != nil {
 140  		return err
 141  	}
 142  
 143  	defer func() {
 144  		// Try to log out in case of errors
 145  		_, _ = d.client.Logout(ctx)
 146  	}()
 147  
 148  	response, err := d.client.GetZone(ctx, zone)
 149  	if err != nil || response.StatusCode != 0 {
 150  		return formatResponseError(response, err)
 151  	}
 152  
 153  	dnsZone, modified := dmapi.RemoveTxtEntryFromZone(response.Body, subDomain)
 154  	if modified {
 155  		response, err = d.client.PutZone(ctx, zone, dnsZone)
 156  		if err != nil || response.StatusCode != 0 {
 157  			return formatResponseError(response, err)
 158  		}
 159  	}
 160  
 161  	response, err = d.client.Logout(ctx)
 162  	if err != nil {
 163  		return formatResponseError(response, err)
 164  	}
 165  
 166  	return nil
 167  }
 168  
 169  // formatResponseError formats error with optional details from DMAPI response.
 170  func formatResponseError(response *dmapi.Response, err error) error {
 171  	if response != nil {
 172  		return fmt.Errorf("joker: DMAPI error: %w Response: %v", err, response.Headers)
 173  	}
 174  
 175  	return fmt.Errorf("joker: DMAPI error: %w", err)
 176  }
 177