ovh.go raw

   1  // Package ovh implements a DNS provider for solving the DNS-01 challenge using OVH DNS.
   2  package ovh
   3  
   4  import (
   5  	"errors"
   6  	"fmt"
   7  	"net/http"
   8  	"sync"
   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/platform/config/env"
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
  15  	"github.com/go-acme/lego/v4/providers/dns/internal/useragent"
  16  	"github.com/ovh/go-ovh/ovh"
  17  )
  18  
  19  // OVH API reference:       https://eu.api.ovh.com/
  20  // Create a Token:          https://eu.api.ovh.com/createToken/
  21  // Create a OAuth2 client:   https://eu.api.ovh.com/console/?section=%2Fme&branch=v1#post-/me/api/oauth2/client
  22  
  23  // Environment variables names.
  24  const (
  25  	envNamespace = "OVH_"
  26  
  27  	EnvEndpoint = envNamespace + "ENDPOINT"
  28  
  29  	EnvTTL                = envNamespace + "TTL"
  30  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  31  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  32  	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
  33  )
  34  
  35  // Authenticate using application key.
  36  const (
  37  	EnvApplicationKey    = envNamespace + "APPLICATION_KEY"
  38  	EnvApplicationSecret = envNamespace + "APPLICATION_SECRET"
  39  	EnvConsumerKey       = envNamespace + "CONSUMER_KEY"
  40  )
  41  
  42  // Authenticate using OAuth2 client.
  43  const (
  44  	EnvClientID     = envNamespace + "CLIENT_ID"
  45  	EnvClientSecret = envNamespace + "CLIENT_SECRET"
  46  )
  47  
  48  // EnvAccessToken Authenticate using Access Token client.
  49  const EnvAccessToken = envNamespace + "ACCESS_TOKEN"
  50  
  51  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  52  
  53  // Record a DNS record.
  54  type Record struct {
  55  	ID        int64  `json:"id,omitempty"`
  56  	FieldType string `json:"fieldType,omitempty"`
  57  	SubDomain string `json:"subDomain,omitempty"`
  58  	Target    string `json:"target,omitempty"`
  59  	TTL       int    `json:"ttl,omitempty"`
  60  	Zone      string `json:"zone,omitempty"`
  61  }
  62  
  63  // OAuth2Config the OAuth2 specific configuration.
  64  type OAuth2Config struct {
  65  	ClientID     string
  66  	ClientSecret string
  67  }
  68  
  69  // Config is used to configure the creation of the DNSProvider.
  70  type Config struct {
  71  	APIEndpoint string
  72  
  73  	ApplicationKey    string
  74  	ApplicationSecret string
  75  	ConsumerKey       string
  76  
  77  	OAuth2Config *OAuth2Config
  78  
  79  	AccessToken string
  80  
  81  	PropagationTimeout time.Duration
  82  	PollingInterval    time.Duration
  83  	TTL                int
  84  	HTTPClient         *http.Client
  85  }
  86  
  87  // NewDefaultConfig returns a default configuration for the DNSProvider.
  88  func NewDefaultConfig() *Config {
  89  	return &Config{
  90  		TTL:                env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
  91  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
  92  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
  93  		HTTPClient: &http.Client{
  94  			Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, ovh.DefaultTimeout),
  95  		},
  96  	}
  97  }
  98  
  99  func (c *Config) hasAppKeyAuth() bool {
 100  	return c.ApplicationKey != "" || c.ApplicationSecret != "" || c.ConsumerKey != ""
 101  }
 102  
 103  // DNSProvider implements the challenge.Provider interface.
 104  type DNSProvider struct {
 105  	config *Config
 106  	client *ovh.Client
 107  
 108  	recordIDs   map[string]int64
 109  	recordIDsMu sync.Mutex
 110  }
 111  
 112  // NewDNSProvider returns a DNSProvider instance configured for OVH
 113  // Credentials must be passed in the environment variables:
 114  // OVH_ENDPOINT (must be either "ovh-eu" or "ovh-ca"), OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY.
 115  func NewDNSProvider() (*DNSProvider, error) {
 116  	config := NewDefaultConfig()
 117  
 118  	// https://github.com/ovh/go-ovh/blob/6817886d12a8c5650794b28da635af9fcdfd1162/ovh/configuration.go#L105
 119  	config.APIEndpoint = env.GetOrDefaultString(EnvEndpoint, "ovh-eu")
 120  
 121  	config.ApplicationKey = env.GetOrFile(EnvApplicationKey)
 122  	config.ApplicationSecret = env.GetOrFile(EnvApplicationSecret)
 123  	config.ConsumerKey = env.GetOrFile(EnvConsumerKey)
 124  
 125  	config.AccessToken = env.GetOrFile(EnvAccessToken)
 126  
 127  	clientID := env.GetOrFile(EnvClientID)
 128  	clientSecret := env.GetOrFile(EnvClientSecret)
 129  
 130  	if clientID != "" || clientSecret != "" {
 131  		config.OAuth2Config = &OAuth2Config{
 132  			ClientID:     clientID,
 133  			ClientSecret: clientSecret,
 134  		}
 135  	}
 136  
 137  	return NewDNSProviderConfig(config)
 138  }
 139  
 140  // NewDNSProviderConfig return a DNSProvider instance configured for OVH.
 141  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
 142  	if config == nil {
 143  		return nil, errors.New("ovh: the configuration of the DNS provider is nil")
 144  	}
 145  
 146  	if config.OAuth2Config != nil && config.hasAppKeyAuth() && config.AccessToken != "" {
 147  		return nil, errors.New("ovh: can't use multiple authentication systems (ApplicationKey, OAuth2, Access Token)")
 148  	}
 149  
 150  	if config.OAuth2Config != nil && config.AccessToken != "" {
 151  		return nil, errors.New("ovh: can't use multiple authentication systems (OAuth2, Access Token)")
 152  	}
 153  
 154  	if config.OAuth2Config != nil && config.hasAppKeyAuth() {
 155  		return nil, errors.New("ovh: can't use multiple authentication systems (ApplicationKey, OAuth2)")
 156  	}
 157  
 158  	if config.hasAppKeyAuth() && config.AccessToken != "" {
 159  		return nil, errors.New("ovh: can't use multiple authentication systems (ApplicationKey, Access Token)")
 160  	}
 161  
 162  	client, err := newClient(config)
 163  	if err != nil {
 164  		return nil, fmt.Errorf("ovh: %w", err)
 165  	}
 166  
 167  	return &DNSProvider{
 168  		config:    config,
 169  		client:    client,
 170  		recordIDs: make(map[string]int64),
 171  	}, nil
 172  }
 173  
 174  // Present creates a TXT record to fulfill the dns-01 challenge.
 175  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
 176  	info := dns01.GetChallengeInfo(domain, keyAuth)
 177  
 178  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 179  	if err != nil {
 180  		return fmt.Errorf("ovh: could not find zone for domain %q: %w", domain, err)
 181  	}
 182  
 183  	authZone = dns01.UnFqdn(authZone)
 184  
 185  	subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
 186  	if err != nil {
 187  		return fmt.Errorf("ovh: %w", err)
 188  	}
 189  
 190  	reqURL := fmt.Sprintf("/domain/zone/%s/record", authZone)
 191  	reqData := Record{FieldType: "TXT", SubDomain: subDomain, Target: info.Value, TTL: d.config.TTL}
 192  
 193  	// Create TXT record
 194  	var respData Record
 195  
 196  	err = d.client.Post(reqURL, reqData, &respData)
 197  	if err != nil {
 198  		return fmt.Errorf("ovh: error when call api to add record (%s): %w", reqURL, err)
 199  	}
 200  
 201  	// Apply the change
 202  	reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone)
 203  
 204  	err = d.client.Post(reqURL, nil, nil)
 205  	if err != nil {
 206  		return fmt.Errorf("ovh: error when call api to refresh zone (%s): %w", reqURL, err)
 207  	}
 208  
 209  	d.recordIDsMu.Lock()
 210  	d.recordIDs[token] = respData.ID
 211  	d.recordIDsMu.Unlock()
 212  
 213  	return nil
 214  }
 215  
 216  // CleanUp removes the TXT record matching the specified parameters.
 217  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 218  	info := dns01.GetChallengeInfo(domain, keyAuth)
 219  
 220  	// get the record's unique ID from when we created it
 221  	d.recordIDsMu.Lock()
 222  	recordID, ok := d.recordIDs[token]
 223  	d.recordIDsMu.Unlock()
 224  
 225  	if !ok {
 226  		return fmt.Errorf("ovh: unknown record ID for '%s'", info.EffectiveFQDN)
 227  	}
 228  
 229  	authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
 230  	if err != nil {
 231  		return fmt.Errorf("ovh: could not find zone for domain %q: %w", domain, err)
 232  	}
 233  
 234  	authZone = dns01.UnFqdn(authZone)
 235  
 236  	reqURL := fmt.Sprintf("/domain/zone/%s/record/%d", authZone, recordID)
 237  
 238  	err = d.client.Delete(reqURL, nil)
 239  	if err != nil {
 240  		return fmt.Errorf("ovh: error when call OVH api to delete challenge record (%s): %w", reqURL, err)
 241  	}
 242  
 243  	// Apply the change
 244  	reqURL = fmt.Sprintf("/domain/zone/%s/refresh", authZone)
 245  
 246  	err = d.client.Post(reqURL, nil, nil)
 247  	if err != nil {
 248  		return fmt.Errorf("ovh: error when call api to refresh zone (%s): %w", reqURL, err)
 249  	}
 250  
 251  	// Delete record ID from map
 252  	d.recordIDsMu.Lock()
 253  	delete(d.recordIDs, token)
 254  	d.recordIDsMu.Unlock()
 255  
 256  	return nil
 257  }
 258  
 259  // Timeout returns the timeout and interval to use when checking for DNS propagation.
 260  // Adjusting here to cope with spikes in propagation times.
 261  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 262  	return d.config.PropagationTimeout, d.config.PollingInterval
 263  }
 264  
 265  func newClient(config *Config) (*ovh.Client, error) {
 266  	var (
 267  		client *ovh.Client
 268  		err    error
 269  	)
 270  
 271  	switch {
 272  	case config.hasAppKeyAuth():
 273  		client, err = ovh.NewClient(config.APIEndpoint, config.ApplicationKey, config.ApplicationSecret, config.ConsumerKey)
 274  	case config.OAuth2Config != nil:
 275  		client, err = ovh.NewOAuth2Client(config.APIEndpoint, config.OAuth2Config.ClientID, config.OAuth2Config.ClientSecret)
 276  	case config.AccessToken != "":
 277  		client, err = ovh.NewAccessTokenClient(config.APIEndpoint, config.AccessToken)
 278  	default:
 279  		client, err = ovh.NewDefaultClient()
 280  	}
 281  
 282  	if err != nil {
 283  		return nil, fmt.Errorf("new client: %w", err)
 284  	}
 285  
 286  	client.UserAgent = useragent.Get()
 287  
 288  	if config.HTTPClient != nil {
 289  		client.Client = config.HTTPClient
 290  	}
 291  
 292  	client.Client = clientdebug.Wrap(client.Client)
 293  
 294  	return client, nil
 295  }
 296