1// Package netcup implements a DNS Provider for solving the DNS-01 challenge using the netcup DNS API. 2package netcup 3 4import ( 5 "errors" 6 "fmt" 7 "net/http" 8 "strings" 9 "time" 10 11 "github.com/go-acme/lego/v3/providers/dns/netcup/internal" 12 13 "github.com/go-acme/lego/v3/challenge/dns01" 14 "github.com/go-acme/lego/v3/log" 15 "github.com/go-acme/lego/v3/platform/config/env" 16) 17 18// Environment variables names. 19const ( 20 envNamespace = "NETCUP_" 21 22 EnvCustomerNumber = envNamespace + "CUSTOMER_NUMBER" 23 EnvAPIKey = envNamespace + "API_KEY" 24 EnvAPIPassword = envNamespace + "API_PASSWORD" 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. 33type Config struct { 34 Key string 35 Password string 36 Customer string 37 TTL int 38 PropagationTimeout time.Duration 39 PollingInterval time.Duration 40 HTTPClient *http.Client 41} 42 43// NewDefaultConfig returns a default configuration for the DNSProvider. 44func NewDefaultConfig() *Config { 45 return &Config{ 46 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), 47 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 120*time.Second), 48 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 5*time.Second), 49 HTTPClient: &http.Client{ 50 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), 51 }, 52 } 53} 54 55// DNSProvider implements the challenge.Provider interface. 56type DNSProvider struct { 57 client *internal.Client 58 config *Config 59} 60 61// NewDNSProvider returns a DNSProvider instance configured for netcup. 62// Credentials must be passed in the environment variables: 63// NETCUP_CUSTOMER_NUMBER, NETCUP_API_KEY, NETCUP_API_PASSWORD. 64func NewDNSProvider() (*DNSProvider, error) { 65 values, err := env.Get(EnvCustomerNumber, EnvAPIKey, EnvAPIPassword) 66 if err != nil { 67 return nil, fmt.Errorf("netcup: %w", err) 68 } 69 70 config := NewDefaultConfig() 71 config.Customer = values[EnvCustomerNumber] 72 config.Key = values[EnvAPIKey] 73 config.Password = values[EnvAPIPassword] 74 75 return NewDNSProviderConfig(config) 76} 77 78// NewDNSProviderConfig return a DNSProvider instance configured for netcup. 79func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { 80 if config == nil { 81 return nil, errors.New("netcup: the configuration of the DNS provider is nil") 82 } 83 84 client, err := internal.NewClient(config.Customer, config.Key, config.Password) 85 if err != nil { 86 return nil, fmt.Errorf("netcup: %w", err) 87 } 88 89 client.HTTPClient = config.HTTPClient 90 91 return &DNSProvider{client: client, config: config}, nil 92} 93 94// Present creates a TXT record to fulfill the dns-01 challenge. 95func (d *DNSProvider) Present(domainName, token, keyAuth string) error { 96 fqdn, value := dns01.GetRecord(domainName, keyAuth) 97 98 zone, err := dns01.FindZoneByFqdn(fqdn) 99 if err != nil { 100 return fmt.Errorf("netcup: failed to find DNSZone, %w", err) 101 } 102 103 sessionID, err := d.client.Login() 104 if err != nil { 105 return fmt.Errorf("netcup: %w", err) 106 } 107 108 defer func() { 109 err = d.client.Logout(sessionID) 110 if err != nil { 111 log.Print("netcup: %v", err) 112 } 113 }() 114 115 hostname := strings.Replace(fqdn, "."+zone, "", 1) 116 record := internal.DNSRecord{ 117 Hostname: hostname, 118 RecordType: "TXT", 119 Destination: value, 120 TTL: d.config.TTL, 121 } 122 123 zone = dns01.UnFqdn(zone) 124 125 records, err := d.client.GetDNSRecords(zone, sessionID) 126 if err != nil { 127 // skip no existing records 128 log.Infof("no existing records, error ignored: %v", err) 129 } 130 131 records = append(records, record) 132 133 err = d.client.UpdateDNSRecord(sessionID, zone, records) 134 if err != nil { 135 return fmt.Errorf("netcup: failed to add TXT-Record: %w", err) 136 } 137 138 return nil 139} 140 141// CleanUp removes the TXT record matching the specified parameters. 142func (d *DNSProvider) CleanUp(domainName, token, keyAuth string) error { 143 fqdn, value := dns01.GetRecord(domainName, keyAuth) 144 145 zone, err := dns01.FindZoneByFqdn(fqdn) 146 if err != nil { 147 return fmt.Errorf("netcup: failed to find DNSZone, %w", err) 148 } 149 150 sessionID, err := d.client.Login() 151 if err != nil { 152 return fmt.Errorf("netcup: %w", err) 153 } 154 155 defer func() { 156 err = d.client.Logout(sessionID) 157 if err != nil { 158 log.Print("netcup: %v", err) 159 } 160 }() 161 162 hostname := strings.Replace(fqdn, "."+zone, "", 1) 163 164 zone = dns01.UnFqdn(zone) 165 166 records, err := d.client.GetDNSRecords(zone, sessionID) 167 if err != nil { 168 return fmt.Errorf("netcup: %w", err) 169 } 170 171 record := internal.DNSRecord{ 172 Hostname: hostname, 173 RecordType: "TXT", 174 Destination: value, 175 } 176 177 idx, err := internal.GetDNSRecordIdx(records, record) 178 if err != nil { 179 return fmt.Errorf("netcup: %w", err) 180 } 181 182 records[idx].DeleteRecord = true 183 184 err = d.client.UpdateDNSRecord(sessionID, zone, []internal.DNSRecord{records[idx]}) 185 if err != nil { 186 return fmt.Errorf("netcup: %w", err) 187 } 188 189 return nil 190} 191 192// Timeout returns the timeout and interval to use when checking for DNS propagation. 193// Adjusting here to cope with spikes in propagation times. 194func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { 195 return d.config.PropagationTimeout, d.config.PollingInterval 196} 197