yandex.go raw
1 // Package yandex implements a DNS provider for solving the DNS-01 challenge using Yandex PDD.
2 package yandex
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
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/yandex/internal"
16 "github.com/miekg/dns"
17 )
18
19 // Environment variables names.
20 const (
21 envNamespace = "YANDEX_"
22
23 EnvPddToken = envNamespace + "PDD_TOKEN"
24
25 EnvTTL = envNamespace + "TTL"
26 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
27 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
28 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
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 PddToken string
36 PropagationTimeout time.Duration
37 PollingInterval time.Duration
38 TTL int
39 HTTPClient *http.Client
40 }
41
42 // NewDefaultConfig returns a default configuration for the DNSProvider.
43 func NewDefaultConfig() *Config {
44 return &Config{
45 TTL: env.GetOrDefaultInt(EnvTTL, 21600),
46 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
47 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
48 HTTPClient: &http.Client{
49 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
50 },
51 }
52 }
53
54 // DNSProvider implements the challenge.Provider interface.
55 type DNSProvider struct {
56 client *internal.Client
57 config *Config
58 }
59
60 // NewDNSProvider returns a DNSProvider instance configured for Yandex.
61 func NewDNSProvider() (*DNSProvider, error) {
62 values, err := env.Get(EnvPddToken)
63 if err != nil {
64 return nil, fmt.Errorf("yandex: %w", err)
65 }
66
67 config := NewDefaultConfig()
68 config.PddToken = values[EnvPddToken]
69
70 return NewDNSProviderConfig(config)
71 }
72
73 // NewDNSProviderConfig return a DNSProvider instance configured for Yandex.
74 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
75 if config == nil {
76 return nil, errors.New("yandex: the configuration of the DNS provider is nil")
77 }
78
79 if config.PddToken == "" {
80 return nil, errors.New("yandex: credentials missing")
81 }
82
83 client, err := internal.NewClient(config.PddToken)
84 if err != nil {
85 return nil, fmt.Errorf("yandex: %w", err)
86 }
87
88 if config.HTTPClient != nil {
89 client.HTTPClient = config.HTTPClient
90 }
91
92 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
93
94 return &DNSProvider{client: client, config: config}, nil
95 }
96
97 // Present creates a TXT record to fulfill the dns-01 challenge.
98 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
99 info := dns01.GetChallengeInfo(domain, keyAuth)
100
101 rootDomain, subDomain, err := splitDomain(info.EffectiveFQDN)
102 if err != nil {
103 return fmt.Errorf("yandex: %w", err)
104 }
105
106 data := internal.Record{
107 Domain: rootDomain,
108 SubDomain: subDomain,
109 Type: "TXT",
110 TTL: d.config.TTL,
111 Content: info.Value,
112 }
113
114 _, err = d.client.AddRecord(context.Background(), data)
115 if err != nil {
116 return fmt.Errorf("yandex: %w", err)
117 }
118
119 return nil
120 }
121
122 // CleanUp removes the TXT record matching the specified parameters.
123 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
124 info := dns01.GetChallengeInfo(domain, keyAuth)
125
126 rootDomain, subDomain, err := splitDomain(info.EffectiveFQDN)
127 if err != nil {
128 return fmt.Errorf("yandex: %w", err)
129 }
130
131 ctx := context.Background()
132
133 records, err := d.client.GetRecords(ctx, rootDomain)
134 if err != nil {
135 return fmt.Errorf("yandex: %w", err)
136 }
137
138 var record *internal.Record
139
140 for _, rcd := range records {
141 if rcd.Type == "TXT" && rcd.SubDomain == subDomain && rcd.Content == info.Value {
142 record = &rcd
143 break
144 }
145 }
146
147 if record == nil {
148 return fmt.Errorf("yandex: TXT record not found for domain: %s", domain)
149 }
150
151 data := internal.Record{
152 ID: record.ID,
153 Domain: rootDomain,
154 }
155
156 _, err = d.client.RemoveRecord(ctx, data)
157 if err != nil {
158 return fmt.Errorf("yandex: %w", err)
159 }
160
161 return nil
162 }
163
164 // Timeout returns the timeout and interval to use when checking for DNS propagation.
165 // Adjusting here to cope with spikes in propagation times.
166 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
167 return d.config.PropagationTimeout, d.config.PollingInterval
168 }
169
170 func splitDomain(full string) (string, string, error) {
171 split := dns.Split(full)
172 if len(split) < 2 {
173 return "", "", fmt.Errorf("unsupported domain: %s", full)
174 }
175
176 if len(split) == 2 {
177 return full, "", nil
178 }
179
180 domain := full[split[len(split)-2]:]
181 subDomain := full[:split[len(split)-2]-1]
182
183 return domain, subDomain, nil
184 }
185