syse.go raw
1 // Package syse implements a DNS provider for solving the DNS-01 challenge using Syse.
2 package syse
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "sync"
10 "time"
11
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/syse/internal"
16 )
17
18 // Environment variables names.
19 const (
20 envNamespace = "SYSE_"
21
22 EnvCredentials = envNamespace + "CREDENTIALS"
23
24 EnvTTL = envNamespace + "TTL"
25 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
26 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
27 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
28 )
29
30 // Config is used to configure the creation of the DNSProvider.
31 type Config struct {
32 Credentials map[string]string
33
34 PropagationTimeout time.Duration
35 PollingInterval time.Duration
36 TTL int
37 HTTPClient *http.Client
38 }
39
40 // NewDefaultConfig returns a default configuration for the DNSProvider.
41 func NewDefaultConfig() *Config {
42 return &Config{
43 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
44 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 1200*time.Second),
45 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
46 HTTPClient: &http.Client{
47 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
48 },
49 }
50 }
51
52 // DNSProvider implements the challenge.Provider interface.
53 type DNSProvider struct {
54 config *Config
55 client *internal.Client
56
57 recordIDs map[string]string
58 recordIDsMu sync.Mutex
59 }
60
61 // NewDNSProvider returns a DNSProvider instance configured for Syse.
62 func NewDNSProvider() (*DNSProvider, error) {
63 values, err := env.Get(EnvCredentials)
64 if err != nil {
65 return nil, fmt.Errorf("syse: %w", err)
66 }
67
68 config := NewDefaultConfig()
69
70 credentials, err := env.ParsePairs(values[EnvCredentials])
71 if err != nil {
72 return nil, fmt.Errorf("syse: credentials: %w", err)
73 }
74
75 config.Credentials = credentials
76
77 return NewDNSProviderConfig(config)
78 }
79
80 // NewDNSProviderConfig return a DNSProvider instance configured for Syse.
81 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
82 if config == nil {
83 return nil, errors.New("syse: the configuration of the DNS provider is nil")
84 }
85
86 if len(config.Credentials) == 0 {
87 return nil, errors.New("syse: missing credentials")
88 }
89
90 for domain, password := range config.Credentials {
91 if domain == "" {
92 return nil, fmt.Errorf(`syse: missing domain: "%s:%s"`, domain, password)
93 }
94
95 if password == "" {
96 return nil, fmt.Errorf(`syse: missing password: "%s:%s"`, domain, password)
97 }
98 }
99
100 client, err := internal.NewClient(config.Credentials)
101 if err != nil {
102 return nil, fmt.Errorf("syse: %w", err)
103 }
104
105 if config.HTTPClient != nil {
106 client.HTTPClient = config.HTTPClient
107 }
108
109 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
110
111 return &DNSProvider{
112 config: config,
113 client: client,
114 recordIDs: make(map[string]string),
115 }, nil
116 }
117
118 // Present creates a TXT record using the specified parameters.
119 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
120 info := dns01.GetChallengeInfo(domain, keyAuth)
121
122 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
123 if err != nil {
124 return fmt.Errorf("syse: could not find zone for domain %q: %w", domain, err)
125 }
126
127 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
128 if err != nil {
129 return fmt.Errorf("syse: %w", err)
130 }
131
132 record := internal.Record{
133 Type: "TXT",
134 Prefix: subDomain,
135 Content: info.Value,
136 TTL: d.config.TTL,
137 Active: true,
138 }
139
140 newRecord, err := d.client.CreateRecord(context.Background(), dns01.UnFqdn(authZone), record)
141 if err != nil {
142 return fmt.Errorf("syse: create record: %w", err)
143 }
144
145 d.recordIDsMu.Lock()
146 d.recordIDs[token] = newRecord.ID
147 d.recordIDsMu.Unlock()
148
149 return nil
150 }
151
152 // CleanUp removes the TXT record matching the specified parameters.
153 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
154 info := dns01.GetChallengeInfo(domain, keyAuth)
155
156 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
157 if err != nil {
158 return fmt.Errorf("syse: could not find zone for domain %q: %w", domain, err)
159 }
160
161 // gets the record's unique ID from when we created it
162 d.recordIDsMu.Lock()
163 recordID, ok := d.recordIDs[token]
164 d.recordIDsMu.Unlock()
165
166 if !ok {
167 return fmt.Errorf("syse: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token)
168 }
169
170 err = d.client.DeleteRecord(context.Background(), dns01.UnFqdn(authZone), recordID)
171 if err != nil {
172 return fmt.Errorf("syse: delete record: %w", err)
173 }
174
175 d.recordIDsMu.Lock()
176 delete(d.recordIDs, token)
177 d.recordIDsMu.Unlock()
178
179 return nil
180 }
181
182 // Timeout returns the timeout and interval to use when checking for DNS propagation.
183 // Adjusting here to cope with spikes in propagation times.
184 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
185 return d.config.PropagationTimeout, d.config.PollingInterval
186 }
187