infomaniak.go raw
1 // Package infomaniak implements a DNS provider for solving the DNS-01 challenge using Infomaniak DNS.
2 package infomaniak
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"
13 "github.com/go-acme/lego/v4/challenge/dns01"
14 "github.com/go-acme/lego/v4/platform/config/env"
15 "github.com/go-acme/lego/v4/providers/dns/infomaniak/internal"
16 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
17 )
18
19 // Infomaniak API reference: https://api.infomaniak.com/doc
20 // Create a Token: https://manager.infomaniak.com/v3/infomaniak-api
21
22 // Environment variables names.
23 const (
24 envNamespace = "INFOMANIAK_"
25
26 EnvEndpoint = envNamespace + "ENDPOINT"
27 EnvAccessToken = envNamespace + "ACCESS_TOKEN"
28
29 EnvTTL = envNamespace + "TTL"
30 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
31 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
32 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
33 )
34
35 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
36
37 // Config is used to configure the creation of the DNSProvider.
38 type Config struct {
39 APIEndpoint string
40 AccessToken string
41 PropagationTimeout time.Duration
42 PollingInterval time.Duration
43 TTL int
44 HTTPClient *http.Client
45 }
46
47 // NewDefaultConfig returns a default configuration for the DNSProvider.
48 func NewDefaultConfig() *Config {
49 return &Config{
50 APIEndpoint: env.GetOrDefaultString(EnvEndpoint, internal.DefaultBaseURL),
51 TTL: env.GetOrDefaultInt(EnvTTL, 300),
52 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
53 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
54 HTTPClient: &http.Client{
55 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
56 },
57 }
58 }
59
60 // DNSProvider implements the challenge.Provider interface.
61 type DNSProvider struct {
62 config *Config
63 client *internal.Client
64
65 recordIDs map[string]string
66 recordIDsMu sync.Mutex
67
68 domainIDs map[string]uint64
69 domainIDsMu sync.Mutex
70 }
71
72 // NewDNSProvider returns a DNSProvider instance configured for Infomaniak.
73 // Credentials must be passed in the environment variables: INFOMANIAK_ACCESS_TOKEN.
74 func NewDNSProvider() (*DNSProvider, error) {
75 values, err := env.Get(EnvAccessToken)
76 if err != nil {
77 return nil, fmt.Errorf("infomaniak: %w", err)
78 }
79
80 config := NewDefaultConfig()
81 config.AccessToken = values[EnvAccessToken]
82
83 return NewDNSProviderConfig(config)
84 }
85
86 // NewDNSProviderConfig return a DNSProvider instance configured for Infomaniak.
87 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
88 if config == nil {
89 return nil, errors.New("infomaniak: the configuration of the DNS provider is nil")
90 }
91
92 if config.APIEndpoint == "" {
93 return nil, errors.New("infomaniak: missing API endpoint")
94 }
95
96 if config.AccessToken == "" {
97 return nil, errors.New("infomaniak: missing access token")
98 }
99
100 client, err := internal.New(
101 clientdebug.Wrap(
102 internal.OAuthStaticAccessToken(config.HTTPClient, config.AccessToken),
103 ),
104 config.APIEndpoint)
105 if err != nil {
106 return nil, fmt.Errorf("infomaniak: %w", err)
107 }
108
109 return &DNSProvider{
110 config: config,
111 client: client,
112 recordIDs: make(map[string]string),
113 domainIDs: make(map[string]uint64),
114 }, nil
115 }
116
117 // Present creates a TXT record to fulfill the dns-01 challenge.
118 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
119 info := dns01.GetChallengeInfo(domain, keyAuth)
120
121 ctx := context.Background()
122
123 ikDomain, err := d.client.GetDomainByName(ctx, dns01.UnFqdn(info.EffectiveFQDN))
124 if err != nil {
125 return fmt.Errorf("infomaniak: could not get domain %q: %w", info.EffectiveFQDN, err)
126 }
127
128 d.domainIDsMu.Lock()
129 d.domainIDs[token] = ikDomain.ID
130 d.domainIDsMu.Unlock()
131
132 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ikDomain.CustomerName)
133 if err != nil {
134 return fmt.Errorf("infomaniak: %w", err)
135 }
136
137 record := internal.Record{
138 Source: subDomain,
139 Target: info.Value,
140 Type: "TXT",
141 TTL: d.config.TTL,
142 }
143
144 recordID, err := d.client.CreateDNSRecord(ctx, ikDomain, record)
145 if err != nil {
146 return fmt.Errorf("infomaniak: error when calling api to create DNS record: %w", err)
147 }
148
149 d.recordIDsMu.Lock()
150 d.recordIDs[token] = recordID
151 d.recordIDsMu.Unlock()
152
153 return nil
154 }
155
156 // CleanUp removes the TXT record matching the specified parameters.
157 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
158 info := dns01.GetChallengeInfo(domain, keyAuth)
159
160 d.recordIDsMu.Lock()
161 recordID, ok := d.recordIDs[token]
162 d.recordIDsMu.Unlock()
163
164 if !ok {
165 return fmt.Errorf("infomaniak: unknown record ID for '%s'", info.EffectiveFQDN)
166 }
167
168 d.domainIDsMu.Lock()
169 domainID, ok := d.domainIDs[token]
170 d.domainIDsMu.Unlock()
171
172 if !ok {
173 return fmt.Errorf("infomaniak: unknown domain ID for '%s'", info.EffectiveFQDN)
174 }
175
176 err := d.client.DeleteDNSRecord(context.Background(), domainID, recordID)
177 if err != nil {
178 return fmt.Errorf("infomaniak: could not delete record %q: %w", dns01.UnFqdn(info.EffectiveFQDN), err)
179 }
180
181 // Delete record ID from map
182 d.recordIDsMu.Lock()
183 delete(d.recordIDs, token)
184 d.recordIDsMu.Unlock()
185
186 // Delete domain ID from map
187 d.domainIDsMu.Lock()
188 delete(d.domainIDs, token)
189 d.domainIDsMu.Unlock()
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