namedotcom.go raw
1 // Package namedotcom implements a DNS provider for solving the DNS-01 challenge using Name.com's DNS service.
2 package namedotcom
3
4 import (
5 "errors"
6 "fmt"
7 "net/http"
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/go-acme/lego/v4/providers/dns/internal/clientdebug"
14 "github.com/namedotcom/go/v4/namecom"
15 )
16
17 // Environment variables names.
18 const (
19 envNamespace = "NAMECOM_"
20
21 EnvUsername = envNamespace + "USERNAME"
22 EnvAPIToken = envNamespace + "API_TOKEN"
23 EnvServer = envNamespace + "SERVER"
24
25 EnvTTL = envNamespace + "TTL"
26 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
27 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
28 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
29 )
30
31 // according to https://www.name.com/api-docs/DNS#CreateRecord
32 const minTTL = 300
33
34 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
35
36 // Config is used to configure the creation of the DNSProvider.
37 type Config struct {
38 Username string
39 APIToken string
40 Server string
41 TTL int
42 PropagationTimeout time.Duration
43 PollingInterval time.Duration
44 HTTPClient *http.Client
45 }
46
47 // NewDefaultConfig returns a default configuration for the DNSProvider.
48 func NewDefaultConfig() *Config {
49 return &Config{
50 TTL: env.GetOrDefaultInt(EnvTTL, minTTL),
51 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 15*time.Minute),
52 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 20*time.Second),
53 HTTPClient: &http.Client{
54 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
55 },
56 }
57 }
58
59 // DNSProvider implements the challenge.Provider interface.
60 type DNSProvider struct {
61 client *namecom.NameCom
62 config *Config
63 }
64
65 // NewDNSProvider returns a DNSProvider instance configured for namedotcom.
66 // Credentials must be passed in the environment variables:
67 // NAMECOM_USERNAME and NAMECOM_API_TOKEN.
68 func NewDNSProvider() (*DNSProvider, error) {
69 values, err := env.Get(EnvUsername, EnvAPIToken)
70 if err != nil {
71 return nil, fmt.Errorf("namedotcom: %w", err)
72 }
73
74 config := NewDefaultConfig()
75 config.Username = values[EnvUsername]
76 config.APIToken = values[EnvAPIToken]
77 config.Server = env.GetOrFile(EnvServer)
78
79 return NewDNSProviderConfig(config)
80 }
81
82 // NewDNSProviderConfig return a DNSProvider instance configured for namedotcom.
83 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
84 if config == nil {
85 return nil, errors.New("namedotcom: the configuration of the DNS provider is nil")
86 }
87
88 if config.Username == "" {
89 return nil, errors.New("namedotcom: username is required")
90 }
91
92 if config.APIToken == "" {
93 return nil, errors.New("namedotcom: API token is required")
94 }
95
96 if config.TTL < minTTL {
97 return nil, fmt.Errorf("namedotcom: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
98 }
99
100 client := namecom.New(config.Username, config.APIToken)
101
102 if config.HTTPClient != nil {
103 client.Client = config.HTTPClient
104 }
105
106 client.Client = clientdebug.Wrap(client.Client)
107
108 if config.Server != "" {
109 client.Server = config.Server
110 }
111
112 return &DNSProvider{client: client, config: config}, nil
113 }
114
115 // Present creates a TXT record to fulfill the dns-01 challenge.
116 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
117 info := dns01.GetChallengeInfo(domain, keyAuth)
118
119 if info.EffectiveFQDN != info.FQDN {
120 domain = dns01.UnFqdn(info.EffectiveFQDN)
121 }
122
123 domainDetails, err := d.client.GetDomain(&namecom.GetDomainRequest{DomainName: domain})
124 if err != nil {
125 return fmt.Errorf("namedotcom: API call failed: %w", err)
126 }
127
128 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, domainDetails.DomainName)
129 if err != nil {
130 return fmt.Errorf("namedotcom: %w", err)
131 }
132
133 request := &namecom.Record{
134 DomainName: domain,
135 Host: subDomain,
136 Type: "TXT",
137 TTL: uint32(d.config.TTL),
138 Answer: info.Value,
139 }
140
141 _, err = d.client.CreateRecord(request)
142 if err != nil {
143 return fmt.Errorf("namedotcom: API call failed: %w", err)
144 }
145
146 return nil
147 }
148
149 // CleanUp removes the TXT record matching the specified parameters.
150 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
151 info := dns01.GetChallengeInfo(domain, keyAuth)
152
153 if info.EffectiveFQDN != info.FQDN {
154 domain = dns01.UnFqdn(info.EffectiveFQDN)
155 }
156
157 records, err := d.getRecords(domain)
158 if err != nil {
159 return fmt.Errorf("namedotcom: %w", err)
160 }
161
162 for _, rec := range records {
163 if rec.Fqdn == info.EffectiveFQDN && rec.Type == "TXT" {
164 request := &namecom.DeleteRecordRequest{
165 DomainName: domain,
166 ID: rec.ID,
167 }
168
169 _, err := d.client.DeleteRecord(request)
170 if err != nil {
171 return fmt.Errorf("namedotcom: %w", err)
172 }
173 }
174 }
175
176 return nil
177 }
178
179 // Timeout returns the timeout and interval to use when checking for DNS propagation.
180 // Adjusting here to cope with spikes in propagation times.
181 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
182 return d.config.PropagationTimeout, d.config.PollingInterval
183 }
184
185 func (d *DNSProvider) getRecords(domain string) ([]*namecom.Record, error) {
186 request := &namecom.ListRecordsRequest{
187 DomainName: domain,
188 Page: 1,
189 }
190
191 var records []*namecom.Record
192
193 for request.Page > 0 {
194 response, err := d.client.ListRecords(request)
195 if err != nil {
196 return nil, err
197 }
198
199 records = append(records, response.Records...)
200 request.Page = response.NextPage
201 }
202
203 return records, nil
204 }
205