bluecat.go raw

   1  // Package bluecat implements a DNS provider for solving the DNS-01 challenge using a self-hosted Bluecat Address Manager.
   2  package bluecat
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"time"
  10  
  11  	"github.com/go-acme/lego/v4/challenge"
  12  	"github.com/go-acme/lego/v4/challenge/dns01"
  13  	"github.com/go-acme/lego/v4/log"
  14  	"github.com/go-acme/lego/v4/platform/config/env"
  15  	"github.com/go-acme/lego/v4/providers/dns/bluecat/internal"
  16  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  17  )
  18  
  19  // Environment variables names.
  20  const (
  21  	envNamespace = "BLUECAT_"
  22  
  23  	EnvServerURL  = envNamespace + "SERVER_URL"
  24  	EnvUserName   = envNamespace + "USER_NAME"
  25  	EnvPassword   = envNamespace + "PASSWORD"
  26  	EnvConfigName = envNamespace + "CONFIG_NAME"
  27  	EnvDNSView    = envNamespace + "DNS_VIEW"
  28  	EnvDebug      = envNamespace + "DEBUG"
  29  	EnvSkipDeploy = envNamespace + "SKIP_DEPLOY"
  30  
  31  	EnvTTL                = envNamespace + "TTL"
  32  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  33  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  34  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  35  )
  36  
  37  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  38  
  39  // Config is used to configure the creation of the DNSProvider.
  40  type Config struct {
  41  	BaseURL            string
  42  	UserName           string
  43  	Password           string
  44  	ConfigName         string
  45  	DNSView            string
  46  	PropagationTimeout time.Duration
  47  	PollingInterval    time.Duration
  48  	TTL                int
  49  	HTTPClient         *http.Client
  50  	Debug              bool
  51  	SkipDeploy         bool
  52  }
  53  
  54  // NewDefaultConfig returns a default configuration for the DNSProvider.
  55  func NewDefaultConfig() *Config {
  56  	return &Config{
  57  		TTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
  58  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  59  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  60  		HTTPClient: &http.Client{
  61  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
  62  		},
  63  		Debug:      env.GetOrDefaultBool(EnvDebug, false),
  64  		SkipDeploy: env.GetOrDefaultBool(EnvSkipDeploy, false),
  65  	}
  66  }
  67  
  68  // DNSProvider implements the challenge.Provider interface.
  69  type DNSProvider struct {
  70  	config *Config
  71  	client *internal.Client
  72  }
  73  
  74  // NewDNSProvider returns a DNSProvider instance configured for Bluecat DNS.
  75  // Credentials must be passed in the environment variables:
  76  //   - BLUECAT_SERVER_URL
  77  //     It should have the scheme, hostname, and port (if required) of the authoritative Bluecat BAM server.
  78  //     The REST endpoint will be appended.
  79  //   - BLUECAT_USER_NAME and BLUECAT_PASSWORD
  80  //   - BLUECAT_CONFIG_NAME (the Configuration name)
  81  //   - BLUECAT_DNS_VIEW (external DNS View Name)
  82  func NewDNSProvider() (*DNSProvider, error) {
  83  	values, err := env.Get(EnvServerURL, EnvUserName, EnvPassword, EnvConfigName, EnvDNSView)
  84  	if err != nil {
  85  		return nil, fmt.Errorf("bluecat: %w", err)
  86  	}
  87  
  88  	config := NewDefaultConfig()
  89  	config.BaseURL = values[EnvServerURL]
  90  	config.UserName = values[EnvUserName]
  91  	config.Password = values[EnvPassword]
  92  	config.ConfigName = values[EnvConfigName]
  93  	config.DNSView = values[EnvDNSView]
  94  
  95  	return NewDNSProviderConfig(config)
  96  }
  97  
  98  // NewDNSProviderConfig return a DNSProvider instance configured for Bluecat DNS.
  99  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
 100  	if config == nil {
 101  		return nil, errors.New("bluecat: the configuration of the DNS provider is nil")
 102  	}
 103  
 104  	if config.BaseURL == "" || config.UserName == "" || config.Password == "" || config.ConfigName == "" || config.DNSView == "" {
 105  		return nil, errors.New("bluecat: credentials missing")
 106  	}
 107  
 108  	client := internal.NewClient(config.BaseURL, config.UserName, config.Password)
 109  
 110  	if config.HTTPClient != nil {
 111  		client.HTTPClient = config.HTTPClient
 112  	}
 113  
 114  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
 115  
 116  	return &DNSProvider{config: config, client: client}, nil
 117  }
 118  
 119  // Present creates a TXT record using the specified parameters
 120  // This will *not* create a sub-zone to contain the TXT record,
 121  // so make sure the FQDN specified is within an existent zone.
 122  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 123  	info := dns01.GetChallengeInfo(domain, keyAuth)
 124  
 125  	ctx, err := d.client.CreateAuthenticatedContext(context.Background())
 126  	if err != nil {
 127  		return fmt.Errorf("bluecat: login: %w", err)
 128  	}
 129  
 130  	viewID, err := d.client.LookupViewID(ctx, d.config.ConfigName, d.config.DNSView)
 131  	if err != nil {
 132  		return fmt.Errorf("bluecat: lookupViewID: %w", err)
 133  	}
 134  
 135  	parentZoneID, name, err := d.client.LookupParentZoneID(ctx, viewID, info.EffectiveFQDN)
 136  	if err != nil {
 137  		return fmt.Errorf("bluecat: lookupParentZoneID: %w", err)
 138  	}
 139  
 140  	if d.config.Debug {
 141  		log.Infof("fqdn: %s; viewID: %d; ZoneID: %d; zone: %s", info.EffectiveFQDN, viewID, parentZoneID, name)
 142  	}
 143  
 144  	txtRecord := internal.Entity{
 145  		Name:       name,
 146  		Type:       internal.TXTType,
 147  		Properties: fmt.Sprintf("ttl=%d|absoluteName=%s|txt=%s|", d.config.TTL, info.EffectiveFQDN, info.Value),
 148  	}
 149  
 150  	_, err = d.client.AddEntity(ctx, parentZoneID, txtRecord)
 151  	if err != nil {
 152  		return fmt.Errorf("bluecat: add TXT record: %w", err)
 153  	}
 154  
 155  	if !d.config.SkipDeploy {
 156  		err = d.client.Deploy(ctx, parentZoneID)
 157  		if err != nil {
 158  			return fmt.Errorf("bluecat: deploy: %w", err)
 159  		}
 160  	}
 161  
 162  	err = d.client.Logout(ctx)
 163  	if err != nil {
 164  		return fmt.Errorf("bluecat: logout: %w", err)
 165  	}
 166  
 167  	return nil
 168  }
 169  
 170  // CleanUp removes the TXT record matching the specified parameters.
 171  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 172  	info := dns01.GetChallengeInfo(domain, keyAuth)
 173  
 174  	ctx, err := d.client.CreateAuthenticatedContext(context.Background())
 175  	if err != nil {
 176  		return fmt.Errorf("bluecat: login: %w", err)
 177  	}
 178  
 179  	viewID, err := d.client.LookupViewID(ctx, d.config.ConfigName, d.config.DNSView)
 180  	if err != nil {
 181  		return fmt.Errorf("bluecat: lookupViewID: %w", err)
 182  	}
 183  
 184  	parentZoneID, name, err := d.client.LookupParentZoneID(ctx, viewID, info.EffectiveFQDN)
 185  	if err != nil {
 186  		return fmt.Errorf("bluecat: lookupParentZoneID: %w", err)
 187  	}
 188  
 189  	txtRecord, err := d.client.GetEntityByName(ctx, parentZoneID, name, internal.TXTType)
 190  	if err != nil {
 191  		return fmt.Errorf("bluecat: get TXT record: %w", err)
 192  	}
 193  
 194  	err = d.client.Delete(ctx, txtRecord.ID)
 195  	if err != nil {
 196  		return fmt.Errorf("bluecat: delete TXT record: %w", err)
 197  	}
 198  
 199  	if !d.config.SkipDeploy {
 200  		err = d.client.Deploy(ctx, parentZoneID)
 201  		if err != nil {
 202  			return fmt.Errorf("bluecat: deploy: %w", err)
 203  		}
 204  	}
 205  
 206  	err = d.client.Logout(ctx)
 207  	if err != nil {
 208  		return fmt.Errorf("bluecat: logout: %w", err)
 209  	}
 210  
 211  	return nil
 212  }
 213  
 214  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 215  // Adjusting here to cope with spikes in propagation times.
 216  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 217  	return d.config.PropagationTimeout, d.config.PollingInterval
 218  }
 219