iijdpf.go raw

   1  // Package iijdpf implements a DNS provider for solving the DNS-01 challenge using IIJ DNS Platform Service.
   2  package iijdpf
   3  
   4  import (
   5  	"context"
   6  	"errors"
   7  	"fmt"
   8  	"time"
   9  
  10  	"github.com/go-acme/lego/v4/challenge"
  11  	"github.com/go-acme/lego/v4/challenge/dns01"
  12  	"github.com/go-acme/lego/v4/platform/config/env"
  13  	"github.com/miekg/dns"
  14  	dpfapi "github.com/mimuret/golang-iij-dpf/pkg/api"
  15  	dpfapiutils "github.com/mimuret/golang-iij-dpf/pkg/apiutils"
  16  )
  17  
  18  // Environment variables names.
  19  const (
  20  	envNamespace = "IIJ_DPF_"
  21  
  22  	EnvAPIToken    = envNamespace + "API_TOKEN"
  23  	EnvServiceCode = envNamespace + "DPM_SERVICE_CODE"
  24  
  25  	EnvAPIEndpoint        = envNamespace + "API_ENDPOINT"
  26  	EnvTTL                = envNamespace + "TTL"
  27  	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
  28  	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
  29  )
  30  
  31  var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
  32  
  33  // Config is used to configure the creation of the DNSProvider.
  34  type Config struct {
  35  	Token       string
  36  	ServiceCode string
  37  
  38  	Endpoint           string
  39  	PropagationTimeout time.Duration
  40  	PollingInterval    time.Duration
  41  	TTL                int
  42  }
  43  
  44  // NewDefaultConfig returns a default configuration for the DNSProvider.
  45  func NewDefaultConfig() *Config {
  46  	return &Config{
  47  		Endpoint:           env.GetOrDefaultString(EnvAPIEndpoint, dpfapi.DefaultEndpoint),
  48  		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 660*time.Second),
  49  		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second),
  50  		TTL:                env.GetOrDefaultInt(EnvTTL, 300),
  51  	}
  52  }
  53  
  54  // DNSProvider implements the challenge.Provider interface.
  55  type DNSProvider struct {
  56  	client dpfapi.ClientInterface
  57  	config *Config
  58  }
  59  
  60  // NewDNSProvider returns a DNSProvider instance configured for IIJ DNS.
  61  func NewDNSProvider() (*DNSProvider, error) {
  62  	values, err := env.Get(EnvAPIToken, EnvServiceCode)
  63  	if err != nil {
  64  		return nil, fmt.Errorf("iijdpf: %w", err)
  65  	}
  66  
  67  	config := NewDefaultConfig()
  68  	config.Token = values[EnvAPIToken]
  69  	config.ServiceCode = values[EnvServiceCode]
  70  
  71  	return NewDNSProviderConfig(config)
  72  }
  73  
  74  // NewDNSProviderConfig takes a given config
  75  // and returns a custom configured DNSProvider instance.
  76  func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
  77  	if config.Token == "" {
  78  		return nil, errors.New("iijdpf: API token missing")
  79  	}
  80  
  81  	if config.ServiceCode == "" {
  82  		return nil, errors.New("iijdpf: Servicecode missing")
  83  	}
  84  
  85  	return &DNSProvider{
  86  		client: dpfapi.NewClient(config.Token, config.Endpoint, nil),
  87  		config: config,
  88  	}, nil
  89  }
  90  
  91  // Timeout returns the timeout and interval to use when checking for DNS propagation.
  92  // Adjusting here to cope with spikes in propagation times.
  93  func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
  94  	return d.config.PropagationTimeout, d.config.PollingInterval
  95  }
  96  
  97  // Present creates a TXT record using the specified parameters.
  98  func (d *DNSProvider) Present(domain, token, keyAuth string) error {
  99  	ctx := context.Background()
 100  
 101  	info := dns01.GetChallengeInfo(domain, keyAuth)
 102  
 103  	zoneID, err := dpfapiutils.GetZoneIdFromServiceCode(ctx, d.client, d.config.ServiceCode)
 104  	if err != nil {
 105  		return fmt.Errorf("iijdpf: failed to get zone id: %w", err)
 106  	}
 107  
 108  	err = d.addTxtRecord(ctx, zoneID, dns.CanonicalName(info.EffectiveFQDN), `"`+info.Value+`"`)
 109  	if err != nil {
 110  		return fmt.Errorf("iijdpf: %w", err)
 111  	}
 112  
 113  	err = d.commit(ctx, zoneID)
 114  	if err != nil {
 115  		return fmt.Errorf("iijdpf: %w", err)
 116  	}
 117  
 118  	return nil
 119  }
 120  
 121  // CleanUp removes the TXT record matching the specified parameters.
 122  func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 123  	ctx := context.Background()
 124  
 125  	info := dns01.GetChallengeInfo(domain, keyAuth)
 126  
 127  	zoneID, err := dpfapiutils.GetZoneIdFromServiceCode(ctx, d.client, d.config.ServiceCode)
 128  	if err != nil {
 129  		return fmt.Errorf("iijdpf: failed to get zone id: %w", err)
 130  	}
 131  
 132  	err = d.deleteTxtRecord(ctx, zoneID, dns.CanonicalName(info.EffectiveFQDN), `"`+info.Value+`"`)
 133  	if err != nil {
 134  		return fmt.Errorf("iijdpf: %w", err)
 135  	}
 136  
 137  	err = d.commit(ctx, zoneID)
 138  	if err != nil {
 139  		return fmt.Errorf("iijdpf: %w", err)
 140  	}
 141  
 142  	return nil
 143  }
 144