otc.go raw
1 // Package otc implements a DNS provider for solving the DNS-01 challenge using Open Telekom Cloud Managed DNS.
2 package otc
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/otc/internal"
16 )
17
18 // Environment variables names.
19 const (
20 envNamespace = "OTC_"
21
22 EnvDomainName = envNamespace + "DOMAIN_NAME"
23 EnvUserName = envNamespace + "USER_NAME"
24 EnvPassword = envNamespace + "PASSWORD"
25 EnvProjectName = envNamespace + "PROJECT_NAME"
26 EnvIdentityEndpoint = envNamespace + "IDENTITY_ENDPOINT"
27 EnvPrivateZone = envNamespace + "PRIVATE_ZONE"
28
29 EnvTTL = envNamespace + "TTL"
30 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
31 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
32 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
33 EnvSequenceInterval = envNamespace + "SEQUENCE_INTERVAL"
34 )
35
36 const defaultIdentityEndpoint = "https://iam.eu-de.otc.t-systems.com:443/v3/auth/tokens"
37
38 // minTTL 300 is otc minimum value for TTL.
39 const minTTL = 300
40
41 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
42
43 // Config is used to configure the creation of the DNSProvider.
44 type Config struct {
45 DomainName string
46 ProjectName string
47 UserName string
48 Password string
49 IdentityEndpoint string
50 PrivateZone bool
51
52 PropagationTimeout time.Duration
53 PollingInterval time.Duration
54 SequenceInterval time.Duration
55 TTL int
56 HTTPClient *http.Client
57 }
58
59 // NewDefaultConfig returns a default configuration for the DNSProvider.
60 func NewDefaultConfig() *Config {
61 tr := &http.Transport{}
62
63 defaultTransport, ok := http.DefaultTransport.(*http.Transport)
64 if ok {
65 tr = defaultTransport.Clone()
66 }
67
68 // Workaround for keep alive bug in otc api
69 tr.DisableKeepAlives = true
70
71 return &Config{
72 PrivateZone: env.GetOrDefaultBool(EnvPrivateZone, false),
73 IdentityEndpoint: env.GetOrDefaultString(EnvIdentityEndpoint, defaultIdentityEndpoint),
74
75 TTL: env.GetOrDefaultInt(EnvTTL, minTTL),
76 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
77 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
78 SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout),
79 HTTPClient: &http.Client{
80 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
81 Transport: tr,
82 },
83 }
84 }
85
86 // DNSProvider implements the challenge.Provider interface.
87 type DNSProvider struct {
88 config *Config
89 client *internal.Client
90 }
91
92 // NewDNSProvider returns a DNSProvider instance configured for OTC DNS.
93 // Credentials must be passed in the environment variables: OTC_USER_NAME,
94 // OTC_DOMAIN_NAME, OTC_PASSWORD OTC_PROJECT_NAME and OTC_IDENTITY_ENDPOINT.
95 func NewDNSProvider() (*DNSProvider, error) {
96 values, err := env.Get(EnvDomainName, EnvUserName, EnvPassword, EnvProjectName)
97 if err != nil {
98 return nil, fmt.Errorf("otc: %w", err)
99 }
100
101 config := NewDefaultConfig()
102 config.DomainName = values[EnvDomainName]
103 config.UserName = values[EnvUserName]
104 config.Password = values[EnvPassword]
105 config.ProjectName = values[EnvProjectName]
106
107 return NewDNSProviderConfig(config)
108 }
109
110 // NewDNSProviderConfig return a DNSProvider instance configured for OTC DNS.
111 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
112 if config == nil {
113 return nil, errors.New("otc: the configuration of the DNS provider is nil")
114 }
115
116 if config.DomainName == "" || config.UserName == "" || config.Password == "" || config.ProjectName == "" {
117 return nil, errors.New("otc: credentials missing")
118 }
119
120 if config.TTL < minTTL {
121 return nil, fmt.Errorf("otc: invalid TTL, TTL (%d) must be greater than %d", config.TTL, minTTL)
122 }
123
124 client := internal.NewClient(config.UserName, config.Password, config.DomainName, config.ProjectName)
125
126 if config.IdentityEndpoint != "" {
127 client.IdentityEndpoint = config.IdentityEndpoint
128 }
129
130 if config.HTTPClient != nil {
131 client.HTTPClient = config.HTTPClient
132 }
133
134 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
135
136 return &DNSProvider{config: config, client: client}, nil
137 }
138
139 // Present creates a TXT record using the specified parameters.
140 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
141 info := dns01.GetChallengeInfo(domain, keyAuth)
142
143 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
144 if err != nil {
145 return fmt.Errorf("otc: could not find zone for domain %q: %w", domain, err)
146 }
147
148 ctx := context.Background()
149
150 err = d.client.Login(ctx)
151 if err != nil {
152 return fmt.Errorf("otc: %w", err)
153 }
154
155 zoneID, err := d.client.GetZoneID(ctx, authZone, d.config.PrivateZone)
156 if err != nil {
157 return fmt.Errorf("otc: unable to get zone: %w", err)
158 }
159
160 record := internal.RecordSets{
161 Name: info.EffectiveFQDN,
162 Description: "Added TXT record for ACME dns-01 challenge using lego client",
163 Type: "TXT",
164 TTL: d.config.TTL,
165 Records: []string{fmt.Sprintf("%q", info.Value)},
166 }
167
168 err = d.client.CreateRecordSet(ctx, zoneID, record)
169 if err != nil {
170 return fmt.Errorf("otc: %w", err)
171 }
172
173 return nil
174 }
175
176 // CleanUp removes the TXT record matching the specified parameters.
177 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
178 info := dns01.GetChallengeInfo(domain, keyAuth)
179
180 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
181 if err != nil {
182 return fmt.Errorf("otc: could not find zone for domain %q: %w", domain, err)
183 }
184
185 ctx := context.Background()
186
187 err = d.client.Login(ctx)
188 if err != nil {
189 return fmt.Errorf("otc: %w", err)
190 }
191
192 zoneID, err := d.client.GetZoneID(ctx, authZone, d.config.PrivateZone)
193 if err != nil {
194 return fmt.Errorf("otc: %w", err)
195 }
196
197 recordID, err := d.client.GetRecordSetID(ctx, zoneID, info.EffectiveFQDN)
198 if err != nil {
199 return fmt.Errorf("otc: unable to get record %s for zone %s: %w", info.EffectiveFQDN, domain, err)
200 }
201
202 err = d.client.DeleteRecordSet(ctx, zoneID, recordID)
203 if err != nil {
204 return fmt.Errorf("otc: %w", err)
205 }
206
207 return nil
208 }
209
210 // Timeout returns the timeout and interval to use when checking for DNS propagation.
211 // Adjusting here to cope with spikes in propagation times.
212 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
213 return d.config.PropagationTimeout, d.config.PollingInterval
214 }
215
216 // Sequential All DNS challenges for this provider will be resolved sequentially.
217 // Returns the interval between each iteration.
218 func (d *DNSProvider) Sequential() time.Duration {
219 return d.config.SequenceInterval
220 }
221