manageengine.go raw

   1  // Package manageengine implements a DNS provider for solving the DNS-01 challenge using ManageEngine CloudDNS.
   2  package manageengine
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"slices"
   9  	"strings"
  10  	"time"
  11  
  12  	"github.com/go-acme/lego/v4/challenge/dns01"
  13  	"github.com/go-acme/lego/v4/platform/config/env"
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  15  	"github.com/go-acme/lego/v4/providers/dns/manageengine/internal"
  16  )
  17  
  18  // Environment variables names.
  19  const (
  20  	envNamespace = "MANAGEENGINE_"
  21  
  22  	EnvClientID     = envNamespace + "CLIENT_ID"
  23  	EnvClientSecret = envNamespace + "CLIENT_SECRET"
  24  
  25  	EnvTTL                = envNamespace + "TTL"
  26  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  27  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  28  )
  29  
  30  // Config is used to configure the creation of the DNSProvider.
  31  type Config struct {
  32  	ClientID     string
  33  	ClientSecret string
  34  
  35  	PropagationTimeout time.Duration
  36  	PollingInterval    time.Duration
  37  	TTL                int
  38  }
  39  
  40  // NewDefaultConfig returns a default configuration for the DNSProvider.
  41  func NewDefaultConfig() *Config {
  42  	return &Config{
  43  		TTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
  44  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  45  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  46  	}
  47  }
  48  
  49  // DNSProvider implements the challenge.Provider interface.
  50  type DNSProvider struct {
  51  	config *Config
  52  	client *internal.Client
  53  }
  54  
  55  // NewDNSProvider returns a DNSProvider instance configured for ManageEngine CloudDNS.
  56  func NewDNSProvider() (*DNSProvider, error) {
  57  	values, err := env.Get(EnvClientID, EnvClientSecret)
  58  	if err != nil {
  59  		return nil, fmt.Errorf("manageengine: %w", err)
  60  	}
  61  
  62  	config := NewDefaultConfig()
  63  	config.ClientID = values[EnvClientID]
  64  	config.ClientSecret = values[EnvClientSecret]
  65  
  66  	return NewDNSProviderConfig(config)
  67  }
  68  
  69  // NewDNSProviderConfig return a DNSProvider instance configured for ManageEngine CloudDNS.
  70  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  71  	if config == nil {
  72  		return nil, errors.New("manageengine: the configuration of the DNS provider is nil")
  73  	}
  74  
  75  	if config.ClientID == "" || config.ClientSecret == "" {
  76  		return nil, errors.New("manageengine: credentials missing")
  77  	}
  78  
  79  	return &DNSProvider{
  80  		config: config,
  81  		client: internal.NewClient(
  82  			clientdebug.Wrap(
  83  				internal.CreateOAuthClient(context.Background(), config.ClientID, config.ClientSecret),
  84  			),
  85  		),
  86  	}, nil
  87  }
  88  
  89  // Present creates a TXT record using the specified parameters.
  90  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
  91  	ctx := context.Background()
  92  
  93  	info := dns01.GetChallengeInfo(domain, keyAuth)
  94  
  95  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
  96  	if err != nil {
  97  		return fmt.Errorf("manageengine: could not find zone for domain %q: %w", domain, err)
  98  	}
  99  
 100  	zoneID, err := d.findZoneID(ctx, authZone)
 101  	if err != nil {
 102  		return fmt.Errorf("manageengine: find zone ID: %w", err)
 103  	}
 104  
 105  	zoneRecord, err := d.findZoneRecord(ctx, zoneID, info.EffectiveFQDN)
 106  	if err != nil {
 107  		return fmt.Errorf("manageengine: find zone record: %w", err)
 108  	}
 109  
 110  	// Update the existing zone record.
 111  	if zoneRecord != nil {
 112  		for _, record := range zoneRecord.Records {
 113  			if slices.Contains(record.Values, info.Value) {
 114  				continue
 115  			}
 116  
 117  			zr := internal.ZoneRecord{
 118  				ZoneID:         zoneID,
 119  				SpfTxtDomainID: zoneRecord.SpfTxtDomainID,
 120  				DomainName:     info.EffectiveFQDN,
 121  				DomainTTL:      d.config.TTL,
 122  				RecordType:     "TXT",
 123  				Records: []internal.Record{{
 124  					Values:   append(record.Values, info.Value),
 125  					DomainID: zoneRecord.SpfTxtDomainID,
 126  				}},
 127  			}
 128  
 129  			// Update the zone record.
 130  			err = d.client.UpdateZoneRecord(ctx, zr)
 131  			if err != nil {
 132  				return fmt.Errorf("manageengine: update zone record: %w", err)
 133  			}
 134  
 135  			return nil
 136  		}
 137  
 138  		return errors.New("manageengine: zone already contains the TXT record value")
 139  	}
 140  
 141  	// Create a new zone record.
 142  	record := internal.ZoneRecord{
 143  		ZoneID:     zoneID,
 144  		DomainName: info.EffectiveFQDN,
 145  		DomainTTL:  d.config.TTL,
 146  		RecordType: "TXT",
 147  		Records: []internal.Record{{
 148  			Values: []string{info.Value},
 149  		}},
 150  	}
 151  
 152  	err = d.client.CreateZoneRecord(ctx, zoneID, record)
 153  	if err != nil {
 154  		return fmt.Errorf("manageengine: create zone record: %w", err)
 155  	}
 156  
 157  	return nil
 158  }
 159  
 160  // CleanUp removes the TXT record matching the specified parameters.
 161  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 162  	ctx := context.Background()
 163  
 164  	info := dns01.GetChallengeInfo(domain, keyAuth)
 165  
 166  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 167  	if err != nil {
 168  		return fmt.Errorf("manageengine: could not find zone for domain %q: %w", domain, err)
 169  	}
 170  
 171  	zoneID, err := d.findZoneID(ctx, authZone)
 172  	if err != nil {
 173  		return fmt.Errorf("manageengine: find zone ID: %w", err)
 174  	}
 175  
 176  	zoneRecord, err := d.findZoneRecord(ctx, zoneID, info.EffectiveFQDN)
 177  	if err != nil {
 178  		return fmt.Errorf("manageengine: find zone record: %w", err)
 179  	}
 180  
 181  	for _, record := range zoneRecord.Records {
 182  		if !slices.Contains(record.Values, info.Value) {
 183  			continue
 184  		}
 185  
 186  		// Delete the zone record.
 187  		if len(record.Values) <= 1 {
 188  			err = d.client.DeleteZoneRecord(ctx, zoneID, zoneRecord.SpfTxtDomainID)
 189  			if err != nil {
 190  				return fmt.Errorf("manageengine: delete zone record: %w", err)
 191  			}
 192  
 193  			return nil
 194  		}
 195  
 196  		// Update the zone record.
 197  		var values []string
 198  
 199  		for _, value := range record.Values {
 200  			if value != info.Value {
 201  				values = append(values, value)
 202  			}
 203  		}
 204  
 205  		zr := internal.ZoneRecord{
 206  			ZoneID:         zoneID,
 207  			SpfTxtDomainID: zoneRecord.SpfTxtDomainID,
 208  			DomainName:     info.EffectiveFQDN,
 209  			DomainTTL:      d.config.TTL,
 210  			RecordType:     "TXT",
 211  			Records: []internal.Record{{
 212  				Values:   values,
 213  				DomainID: zoneRecord.SpfTxtDomainID,
 214  			}},
 215  		}
 216  
 217  		err = d.client.UpdateZoneRecord(ctx, zr)
 218  		if err != nil {
 219  			return fmt.Errorf("manageengine: create zone record: %w", err)
 220  		}
 221  
 222  		return nil
 223  	}
 224  
 225  	return nil
 226  }
 227  
 228  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 229  // Adjusting here to cope with spikes in propagation times.
 230  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 231  	return d.config.PropagationTimeout, d.config.PollingInterval
 232  }
 233  
 234  func (d *DNSProvider) findZoneID(ctx context.Context, authZone string) (int, error) {
 235  	zones, err := d.client.GetAllZones(ctx)
 236  	if err != nil {
 237  		return 0, fmt.Errorf("get all zone groups: %w", err)
 238  	}
 239  
 240  	for _, zone := range zones {
 241  		if strings.EqualFold(zone.ZoneName, authZone) {
 242  			return zone.ZoneID, nil
 243  		}
 244  	}
 245  
 246  	return 0, fmt.Errorf("zone not found %s", authZone)
 247  }
 248  
 249  func (d *DNSProvider) findZoneRecord(ctx context.Context, zoneID int, fqdn string) (*internal.ZoneRecord, error) {
 250  	zoneRecords, err := d.client.GetAllZoneRecords(ctx, zoneID)
 251  	if err != nil {
 252  		return nil, fmt.Errorf("get all zone records: %w", err)
 253  	}
 254  
 255  	for _, zoneRecord := range zoneRecords {
 256  		if !strings.EqualFold(zoneRecord.DomainName, fqdn) {
 257  			continue
 258  		}
 259  
 260  		if strings.EqualFold(zoneRecord.RecordType, "TXT") {
 261  			return &zoneRecord, nil
 262  		}
 263  	}
 264  
 265  	return nil, nil
 266  }
 267