gigahostno.go raw

   1  // Package gigahostno implements a DNS provider for solving the DNS-01 challenge using Gigahost.no.
   2  package gigahostno
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"net/http"
   9  	"sync"
  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/gigahostno/internal"
  15  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  16  )
  17  
  18  // Environment variables names.
  19  const (
  20  	envNamespace = "GIGAHOSTNO_"
  21  
  22  	EnvUsername = envNamespace + "USERNAME"
  23  	EnvPassword = envNamespace + "PASSWORD"
  24  	EnvSecret   = envNamespace + "SECRET"
  25  
  26  	EnvTTL                = envNamespace + "TTL"
  27  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  28  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  29  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  30  )
  31  
  32  // Config is used to configure the creation of the DNSProvider.
  33  type Config struct {
  34  	Username string
  35  	Password string
  36  	Secret   string
  37  
  38  	PropagationTimeout time.Duration
  39  	PollingInterval    time.Duration
  40  	TTL                int
  41  	HTTPClient         *http.Client
  42  }
  43  
  44  // NewDefaultConfig returns a default configuration for the DNSProvider.
  45  func NewDefaultConfig() *Config {
  46  	return &Config{
  47  		TTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
  48  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  49  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  50  		HTTPClient: &http.Client{
  51  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
  52  		},
  53  	}
  54  }
  55  
  56  // DNSProvider implements the challenge.Provider interface.
  57  type DNSProvider struct {
  58  	config *Config
  59  
  60  	identifier *internal.Identifier
  61  	client     *internal.Client
  62  
  63  	tokenMu sync.Mutex
  64  	token   *internal.Token
  65  }
  66  
  67  // NewDNSProvider returns a DNSProvider instance configured for Gigahost.
  68  func NewDNSProvider() (*DNSProvider, error) {
  69  	values, err := env.Get(EnvUsername, EnvPassword)
  70  	if err != nil {
  71  		return nil, fmt.Errorf("gigahostno: %w", err)
  72  	}
  73  
  74  	config := NewDefaultConfig()
  75  	config.Username = values[EnvUsername]
  76  	config.Password = values[EnvPassword]
  77  	config.Secret = env.GetOrFile(EnvSecret)
  78  
  79  	return NewDNSProviderConfig(config)
  80  }
  81  
  82  // NewDNSProviderConfig return a DNSProvider instance configured for Gigahost.
  83  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  84  	if config == nil {
  85  		return nil, errors.New("gigahostno: the configuration of the DNS provider is nil")
  86  	}
  87  
  88  	identifier, err := internal.NewIdentifier(config.Username, config.Password, config.Secret)
  89  	if err != nil {
  90  		return nil, fmt.Errorf("gigahostno: %w", err)
  91  	}
  92  
  93  	if config.HTTPClient != nil {
  94  		identifier.HTTPClient = config.HTTPClient
  95  	}
  96  
  97  	identifier.HTTPClient = clientdebug.Wrap(identifier.HTTPClient)
  98  
  99  	client := internal.NewClient()
 100  
 101  	if config.HTTPClient != nil {
 102  		client.HTTPClient = config.HTTPClient
 103  	}
 104  
 105  	client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
 106  
 107  	return &DNSProvider{
 108  		config:     config,
 109  		identifier: identifier,
 110  		client:     client,
 111  	}, nil
 112  }
 113  
 114  // Present creates a TXT record using the specified parameters.
 115  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 116  	ctx := context.Background()
 117  
 118  	info := dns01.GetChallengeInfo(domain, keyAuth)
 119  
 120  	err := d.authenticate(ctx)
 121  	if err != nil {
 122  		return fmt.Errorf("gigahostno: %w", err)
 123  	}
 124  
 125  	ctx = internal.WithContext(ctx, d.token.Token)
 126  
 127  	zone, err := d.findZone(ctx, info.EffectiveFQDN)
 128  	if err != nil {
 129  		return fmt.Errorf("gigahostno: %w", err)
 130  	}
 131  
 132  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)
 133  	if err != nil {
 134  		return fmt.Errorf("gigahostno: %w", err)
 135  	}
 136  
 137  	record := internal.Record{
 138  		Name:  subDomain,
 139  		Type:  "TXT",
 140  		Value: info.Value,
 141  		TTL:   d.config.TTL,
 142  	}
 143  
 144  	err = d.client.CreateNewRecord(ctx, zone.ID, record)
 145  	if err != nil {
 146  		return fmt.Errorf("gigahostno: create new record: %w", err)
 147  	}
 148  
 149  	return nil
 150  }
 151  
 152  // CleanUp removes the TXT record matching the specified parameters.
 153  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 154  	ctx := context.Background()
 155  
 156  	info := dns01.GetChallengeInfo(domain, keyAuth)
 157  
 158  	err := d.authenticate(ctx)
 159  	if err != nil {
 160  		return fmt.Errorf("gigahostno: %w", err)
 161  	}
 162  
 163  	ctx = internal.WithContext(ctx, d.token.Token)
 164  
 165  	zone, err := d.findZone(ctx, info.EffectiveFQDN)
 166  	if err != nil {
 167  		return fmt.Errorf("gigahostno: %w", err)
 168  	}
 169  
 170  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)
 171  	if err != nil {
 172  		return fmt.Errorf("gigahostno: %w", err)
 173  	}
 174  
 175  	records, err := d.client.GetZoneRecords(ctx, zone.ID)
 176  	if err != nil {
 177  		return fmt.Errorf("gigahostno: get zone records: %w", err)
 178  	}
 179  
 180  	for _, record := range records {
 181  		if record.Type == "TXT" && record.Name == subDomain && record.Value == info.Value {
 182  			err := d.client.DeleteRecord(ctx, zone.ID, record.ID, record.Name, record.Type)
 183  			if err != nil {
 184  				return fmt.Errorf("gigahostno: delete record: %w", err)
 185  			}
 186  
 187  			break
 188  		}
 189  	}
 190  
 191  	return nil
 192  }
 193  
 194  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 195  // Adjusting here to cope with spikes in propagation times.
 196  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 197  	return d.config.PropagationTimeout, d.config.PollingInterval
 198  }
 199  
 200  func (d *DNSProvider) authenticate(ctx context.Context) error {
 201  	d.tokenMu.Lock()
 202  	defer d.tokenMu.Unlock()
 203  
 204  	if !d.token.IsExpired() {
 205  		return nil
 206  	}
 207  
 208  	tok, err := d.identifier.Authenticate(ctx)
 209  	if err != nil {
 210  		return fmt.Errorf("authenticate: %w", err)
 211  	}
 212  
 213  	d.token = tok
 214  
 215  	return nil
 216  }
 217  
 218  func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.Zone, error) {
 219  	zones, err := d.client.GetZones(ctx)
 220  	if err != nil {
 221  		return nil, fmt.Errorf("get zones: %w", err)
 222  	}
 223  
 224  	for d := range dns01.UnFqdnDomainsSeq(fqdn) {
 225  		for _, zone := range zones {
 226  			if zone.Name == d && zone.Active == "1" {
 227  				return &zone, nil
 228  			}
 229  		}
 230  	}
 231  
 232  	return nil, fmt.Errorf("zone not found for %q", fqdn)
 233  }
 234