infomaniak.go raw

   1  // Package infomaniak implements a DNS provider for solving the DNS-01 challenge using Infomaniak DNS.
   2  package infomaniak
   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"
  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/infomaniak/internal"
  16  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  17  )
  18  
  19  // Infomaniak API reference: https://api.infomaniak.com/doc
  20  // Create a Token: https://manager.infomaniak.com/v3/infomaniak-api
  21  
  22  // Environment variables names.
  23  const (
  24  	envNamespace = "INFOMANIAK_"
  25  
  26  	EnvEndpoint    = envNamespace + "ENDPOINT"
  27  	EnvAccessToken = envNamespace + "ACCESS_TOKEN"
  28  
  29  	EnvTTL                = envNamespace + "TTL"
  30  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  31  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  32  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  33  )
  34  
  35  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  36  
  37  // Config is used to configure the creation of the DNSProvider.
  38  type Config struct {
  39  	APIEndpoint        string
  40  	AccessToken        string
  41  	PropagationTimeout time.Duration
  42  	PollingInterval    time.Duration
  43  	TTL                int
  44  	HTTPClient         *http.Client
  45  }
  46  
  47  // NewDefaultConfig returns a default configuration for the DNSProvider.
  48  func NewDefaultConfig() *Config {
  49  	return &Config{
  50  		APIEndpoint:        env.GetOrDefaultString(EnvEndpoint, internal.DefaultBaseURL),
  51  		TTL:                env.GetOrDefaultInt(EnvTTL, 300),
  52  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
  53  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
  54  		HTTPClient: &http.Client{
  55  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
  56  		},
  57  	}
  58  }
  59  
  60  // DNSProvider implements the challenge.Provider interface.
  61  type DNSProvider struct {
  62  	config *Config
  63  	client *internal.Client
  64  
  65  	recordIDs   map[string]string
  66  	recordIDsMu sync.Mutex
  67  
  68  	domainIDs   map[string]uint64
  69  	domainIDsMu sync.Mutex
  70  }
  71  
  72  // NewDNSProvider returns a DNSProvider instance configured for Infomaniak.
  73  // Credentials must be passed in the environment variables: INFOMANIAK_ACCESS_TOKEN.
  74  func NewDNSProvider() (*DNSProvider, error) {
  75  	values, err := env.Get(EnvAccessToken)
  76  	if err != nil {
  77  		return nil, fmt.Errorf("infomaniak: %w", err)
  78  	}
  79  
  80  	config := NewDefaultConfig()
  81  	config.AccessToken = values[EnvAccessToken]
  82  
  83  	return NewDNSProviderConfig(config)
  84  }
  85  
  86  // NewDNSProviderConfig return a DNSProvider instance configured for Infomaniak.
  87  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  88  	if config == nil {
  89  		return nil, errors.New("infomaniak: the configuration of the DNS provider is nil")
  90  	}
  91  
  92  	if config.APIEndpoint == "" {
  93  		return nil, errors.New("infomaniak: missing API endpoint")
  94  	}
  95  
  96  	if config.AccessToken == "" {
  97  		return nil, errors.New("infomaniak: missing access token")
  98  	}
  99  
 100  	client, err := internal.New(
 101  		clientdebug.Wrap(
 102  			internal.OAuthStaticAccessToken(config.HTTPClient, config.AccessToken),
 103  		),
 104  		config.APIEndpoint)
 105  	if err != nil {
 106  		return nil, fmt.Errorf("infomaniak: %w", err)
 107  	}
 108  
 109  	return &DNSProvider{
 110  		config:    config,
 111  		client:    client,
 112  		recordIDs: make(map[string]string),
 113  		domainIDs: make(map[string]uint64),
 114  	}, nil
 115  }
 116  
 117  // Present creates a TXT record to fulfill the dns-01 challenge.
 118  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 119  	info := dns01.GetChallengeInfo(domain, keyAuth)
 120  
 121  	ctx := context.Background()
 122  
 123  	ikDomain, err := d.client.GetDomainByName(ctx, dns01.UnFqdn(info.EffectiveFQDN))
 124  	if err != nil {
 125  		return fmt.Errorf("infomaniak: could not get domain %q: %w", info.EffectiveFQDN, err)
 126  	}
 127  
 128  	d.domainIDsMu.Lock()
 129  	d.domainIDs[token] = ikDomain.ID
 130  	d.domainIDsMu.Unlock()
 131  
 132  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ikDomain.CustomerName)
 133  	if err != nil {
 134  		return fmt.Errorf("infomaniak: %w", err)
 135  	}
 136  
 137  	record := internal.Record{
 138  		Source: subDomain,
 139  		Target: info.Value,
 140  		Type:   "TXT",
 141  		TTL:    d.config.TTL,
 142  	}
 143  
 144  	recordID, err := d.client.CreateDNSRecord(ctx, ikDomain, record)
 145  	if err != nil {
 146  		return fmt.Errorf("infomaniak: error when calling api to create DNS record: %w", err)
 147  	}
 148  
 149  	d.recordIDsMu.Lock()
 150  	d.recordIDs[token] = recordID
 151  	d.recordIDsMu.Unlock()
 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  	d.recordIDsMu.Lock()
 161  	recordID, ok := d.recordIDs[token]
 162  	d.recordIDsMu.Unlock()
 163  
 164  	if !ok {
 165  		return fmt.Errorf("infomaniak: unknown record ID for '%s'", info.EffectiveFQDN)
 166  	}
 167  
 168  	d.domainIDsMu.Lock()
 169  	domainID, ok := d.domainIDs[token]
 170  	d.domainIDsMu.Unlock()
 171  
 172  	if !ok {
 173  		return fmt.Errorf("infomaniak: unknown domain ID for '%s'", info.EffectiveFQDN)
 174  	}
 175  
 176  	err := d.client.DeleteDNSRecord(context.Background(), domainID, recordID)
 177  	if err != nil {
 178  		return fmt.Errorf("infomaniak: could not delete record %q: %w", dns01.UnFqdn(info.EffectiveFQDN), err)
 179  	}
 180  
 181  	// Delete record ID from map
 182  	d.recordIDsMu.Lock()
 183  	delete(d.recordIDs, token)
 184  	d.recordIDsMu.Unlock()
 185  
 186  	// Delete domain ID from map
 187  	d.domainIDsMu.Lock()
 188  	delete(d.domainIDs, token)
 189  	d.domainIDsMu.Unlock()
 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