mythicbeasts.go raw

   1  // Package mythicbeasts implements a DNS provider for solving the DNS-01 challenge using Mythic Beasts API.
   2  package mythicbeasts
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"net/url"
  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/mythicbeasts/internal"
  17  )
  18  
  19  // Environment variables names.
  20  const (
  21  	envNamespace = "MYTHICBEASTS_"
  22  
  23  	EnvUserName        = envNamespace + "USERNAME"
  24  	EnvPassword        = envNamespace + "PASSWORD"
  25  	EnvAPIEndpoint     = envNamespace + "API_ENDPOINT"
  26  	EnvAuthAPIEndpoint = envNamespace + "AUTH_API_ENDPOINT"
  27  
  28  	EnvTTL                = envNamespace + "TTL"
  29  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  30  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  31  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  32  )
  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  	HTTPClient         *http.Client
  41  	PropagationTimeout time.Duration
  42  	PollingInterval    time.Duration
  43  	APIEndpoint        *url.URL
  44  	AuthAPIEndpoint    *url.URL
  45  	TTL                int
  46  }
  47  
  48  // NewDefaultConfig returns a default configuration for the DNSProvider.
  49  func NewDefaultConfig() (*Config, error) {
  50  	apiEndpoint, err := url.Parse(env.GetOrDefaultString(EnvAPIEndpoint, internal.APIBaseURL))
  51  	if err != nil {
  52  		return nil, fmt.Errorf("mythicbeasts: Unable to parse API URL: %w", err)
  53  	}
  54  
  55  	authEndpoint, err := url.Parse(env.GetOrDefaultString(EnvAuthAPIEndpoint, internal.AuthBaseURL))
  56  	if err != nil {
  57  		return nil, fmt.Errorf("mythicbeasts: Unable to parse AUTH API URL: %w", err)
  58  	}
  59  
  60  	return &Config{
  61  		TTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
  62  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  63  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  64  		APIEndpoint:        apiEndpoint,
  65  		AuthAPIEndpoint:    authEndpoint,
  66  		HTTPClient: &http.Client{
  67  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
  68  		},
  69  	}, nil
  70  }
  71  
  72  // DNSProvider implements the challenge.Provider interface.
  73  type DNSProvider struct {
  74  	config *Config
  75  	client *internal.Client
  76  }
  77  
  78  // NewDNSProvider returns a DNSProvider instance configured for mythicbeasts DNSv2 API.
  79  // Credentials must be passed in the environment variables:
  80  // MYTHICBEASTS_USERNAME and MYTHICBEASTS_PASSWORD.
  81  func NewDNSProvider() (*DNSProvider, error) {
  82  	values, err := env.Get(EnvUserName, EnvPassword)
  83  	if err != nil {
  84  		return nil, fmt.Errorf("mythicbeasts: %w", err)
  85  	}
  86  
  87  	config, err := NewDefaultConfig()
  88  	if err != nil {
  89  		return nil, fmt.Errorf("mythicbeasts: %w", err)
  90  	}
  91  
  92  	config.UserName = values[EnvUserName]
  93  	config.Password = values[EnvPassword]
  94  
  95  	return NewDNSProviderConfig(config)
  96  }
  97  
  98  // NewDNSProviderConfig return a DNSProvider instance configured for mythicbeasts DNSv2 API.
  99  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
 100  	if config == nil {
 101  		return nil, errors.New("mythicbeasts: the configuration of the DNS provider is nil")
 102  	}
 103  
 104  	if config.UserName == "" || config.Password == "" {
 105  		return nil, errors.New("mythicbeasts: incomplete credentials, missing username and/or password")
 106  	}
 107  
 108  	client := internal.NewClient(config.UserName, config.Password)
 109  
 110  	if config.APIEndpoint != nil {
 111  		client.APIEndpoint = config.APIEndpoint
 112  	}
 113  
 114  	if config.AuthAPIEndpoint != nil {
 115  		client.AuthEndpoint = config.AuthAPIEndpoint
 116  	}
 117  
 118  	if config.HTTPClient != nil {
 119  		client.HTTPClient = config.HTTPClient
 120  	}
 121  
 122  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
 123  
 124  	return &DNSProvider{config: config, client: client}, nil
 125  }
 126  
 127  // Present creates a TXT record using the specified parameters.
 128  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 129  	info := dns01.GetChallengeInfo(domain, keyAuth)
 130  
 131  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 132  	if err != nil {
 133  		return fmt.Errorf("mythicbeasts: could not find zone for domain %q: %w", domain, err)
 134  	}
 135  
 136  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
 137  	if err != nil {
 138  		return fmt.Errorf("mythicbeasts: %w", err)
 139  	}
 140  
 141  	authZone = dns01.UnFqdn(authZone)
 142  
 143  	ctx, err := d.client.CreateAuthenticatedContext(context.Background())
 144  	if err != nil {
 145  		return fmt.Errorf("mythicbeasts: login: %w", err)
 146  	}
 147  
 148  	err = d.client.CreateTXTRecord(ctx, authZone, subDomain, info.Value, d.config.TTL)
 149  	if err != nil {
 150  		return fmt.Errorf("mythicbeasts: CreateTXTRecord: %w", err)
 151  	}
 152  
 153  	return nil
 154  }
 155  
 156  // CleanUp removes the TXT record matching the specified parameters.
 157  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 158  	info := dns01.GetChallengeInfo(domain, keyAuth)
 159  
 160  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 161  	if err != nil {
 162  		return fmt.Errorf("mythicbeasts: could not find zone for domain %q: %w", domain, err)
 163  	}
 164  
 165  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
 166  	if err != nil {
 167  		return fmt.Errorf("mythicbeasts: %w", err)
 168  	}
 169  
 170  	authZone = dns01.UnFqdn(authZone)
 171  
 172  	ctx, err := d.client.CreateAuthenticatedContext(context.Background())
 173  	if err != nil {
 174  		return fmt.Errorf("mythicbeasts: login: %w", err)
 175  	}
 176  
 177  	err = d.client.RemoveTXTRecord(ctx, authZone, subDomain, info.Value)
 178  	if err != nil {
 179  		return fmt.Errorf("mythicbeasts: RemoveTXTRecord: %w", err)
 180  	}
 181  
 182  	return nil
 183  }
 184  
 185  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 186  // Adjusting here to cope with spikes in propagation times.
 187  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 188  	return d.config.PropagationTimeout, d.config.PollingInterval
 189  }
 190