gigahostno.go raw
1 // Package gigahostno implements a DNS provider for solving the DNS-01 challenge using Gigahost.no.
2 package gigahostno
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/gigahostno/internal"
15 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
16 )
17
18 // Environment variables names.
19 const (
20 envNamespace = "GIGAHOSTNO_"
21
22 EnvUsername = envNamespace + "USERNAME"
23 EnvPassword = envNamespace + "PASSWORD"
24 EnvSecret = envNamespace + "SECRET"
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.
33 type Config struct {
34 Username string
35 Password string
36 Secret string
37
38 PropagationTimeout time.Duration
39 PollingInterval time.Duration
40 TTL int
41 HTTPClient *http.Client
42 }
43
44 // NewDefaultConfig returns a default configuration for the DNSProvider.
45 func NewDefaultConfig() *Config {
46 return &Config{
47 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
48 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
49 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
50 HTTPClient: &http.Client{
51 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
52 },
53 }
54 }
55
56 // DNSProvider implements the challenge.Provider interface.
57 type DNSProvider struct {
58 config *Config
59
60 identifier *internal.Identifier
61 client *internal.Client
62
63 tokenMu sync.Mutex
64 token *internal.Token
65 }
66
67 // NewDNSProvider returns a DNSProvider instance configured for Gigahost.
68 func NewDNSProvider() (*DNSProvider, error) {
69 values, err := env.Get(EnvUsername, EnvPassword)
70 if err != nil {
71 return nil, fmt.Errorf("gigahostno: %w", err)
72 }
73
74 config := NewDefaultConfig()
75 config.Username = values[EnvUsername]
76 config.Password = values[EnvPassword]
77 config.Secret = env.GetOrFile(EnvSecret)
78
79 return NewDNSProviderConfig(config)
80 }
81
82 // NewDNSProviderConfig return a DNSProvider instance configured for Gigahost.
83 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
84 if config == nil {
85 return nil, errors.New("gigahostno: the configuration of the DNS provider is nil")
86 }
87
88 identifier, err := internal.NewIdentifier(config.Username, config.Password, config.Secret)
89 if err != nil {
90 return nil, fmt.Errorf("gigahostno: %w", err)
91 }
92
93 if config.HTTPClient != nil {
94 identifier.HTTPClient = config.HTTPClient
95 }
96
97 identifier.HTTPClient = clientdebug.Wrap(identifier.HTTPClient)
98
99 client := internal.NewClient()
100
101 if config.HTTPClient != nil {
102 client.HTTPClient = config.HTTPClient
103 }
104
105 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
106
107 return &DNSProvider{
108 config: config,
109 identifier: identifier,
110 client: client,
111 }, nil
112 }
113
114 // Present creates a TXT record using the specified parameters.
115 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
116 ctx := context.Background()
117
118 info := dns01.GetChallengeInfo(domain, keyAuth)
119
120 err := d.authenticate(ctx)
121 if err != nil {
122 return fmt.Errorf("gigahostno: %w", err)
123 }
124
125 ctx = internal.WithContext(ctx, d.token.Token)
126
127 zone, err := d.findZone(ctx, info.EffectiveFQDN)
128 if err != nil {
129 return fmt.Errorf("gigahostno: %w", err)
130 }
131
132 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)
133 if err != nil {
134 return fmt.Errorf("gigahostno: %w", err)
135 }
136
137 record := internal.Record{
138 Name: subDomain,
139 Type: "TXT",
140 Value: info.Value,
141 TTL: d.config.TTL,
142 }
143
144 err = d.client.CreateNewRecord(ctx, zone.ID, record)
145 if err != nil {
146 return fmt.Errorf("gigahostno: create new record: %w", err)
147 }
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 ctx := context.Background()
155
156 info := dns01.GetChallengeInfo(domain, keyAuth)
157
158 err := d.authenticate(ctx)
159 if err != nil {
160 return fmt.Errorf("gigahostno: %w", err)
161 }
162
163 ctx = internal.WithContext(ctx, d.token.Token)
164
165 zone, err := d.findZone(ctx, info.EffectiveFQDN)
166 if err != nil {
167 return fmt.Errorf("gigahostno: %w", err)
168 }
169
170 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)
171 if err != nil {
172 return fmt.Errorf("gigahostno: %w", err)
173 }
174
175 records, err := d.client.GetZoneRecords(ctx, zone.ID)
176 if err != nil {
177 return fmt.Errorf("gigahostno: get zone records: %w", err)
178 }
179
180 for _, record := range records {
181 if record.Type == "TXT" && record.Name == subDomain && record.Value == info.Value {
182 err := d.client.DeleteRecord(ctx, zone.ID, record.ID, record.Name, record.Type)
183 if err != nil {
184 return fmt.Errorf("gigahostno: delete record: %w", err)
185 }
186
187 break
188 }
189 }
190
191 return nil
192 }
193
194 // Timeout returns the timeout and interval to use when checking for DNS propagation.
195 // Adjusting here to cope with spikes in propagation times.
196 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
197 return d.config.PropagationTimeout, d.config.PollingInterval
198 }
199
200 func (d *DNSProvider) authenticate(ctx context.Context) error {
201 d.tokenMu.Lock()
202 defer d.tokenMu.Unlock()
203
204 if !d.token.IsExpired() {
205 return nil
206 }
207
208 tok, err := d.identifier.Authenticate(ctx)
209 if err != nil {
210 return fmt.Errorf("authenticate: %w", err)
211 }
212
213 d.token = tok
214
215 return nil
216 }
217
218 func (d *DNSProvider) findZone(ctx context.Context, fqdn string) (*internal.Zone, error) {
219 zones, err := d.client.GetZones(ctx)
220 if err != nil {
221 return nil, fmt.Errorf("get zones: %w", err)
222 }
223
224 for d := range dns01.UnFqdnDomainsSeq(fqdn) {
225 for _, zone := range zones {
226 if zone.Name == d && zone.Active == "1" {
227 return &zone, nil
228 }
229 }
230 }
231
232 return nil, fmt.Errorf("zone not found for %q", fqdn)
233 }
234