rackspace.go raw
1 // Package rackspace implements a DNS provider for solving the DNS-01 challenge using rackspace DNS.
2 package rackspace
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/rackspace/internal"
16 )
17
18 // Environment variables names.
19 const (
20 envNamespace = "RACKSPACE_"
21
22 EnvUser = envNamespace + "USER"
23 EnvAPIKey = envNamespace + "API_KEY"
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 BaseURL string
36 APIUser string
37 APIKey string
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 BaseURL: internal.DefaultIdentityURL,
48 TTL: env.GetOrDefaultInt(EnvTTL, 300),
49 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
50 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
51 HTTPClient: &http.Client{
52 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
53 },
54 }
55 }
56
57 // DNSProvider implements the challenge.Provider interface.
58 type DNSProvider struct {
59 config *Config
60 client *internal.Client
61
62 token string
63 cloudDNSEndpoint string
64 }
65
66 // NewDNSProvider returns a DNSProvider instance configured for Rackspace.
67 // Credentials must be passed in the environment variables:
68 // RACKSPACE_USER and RACKSPACE_API_KEY.
69 func NewDNSProvider() (*DNSProvider, error) {
70 values, err := env.Get(EnvUser, EnvAPIKey)
71 if err != nil {
72 return nil, fmt.Errorf("rackspace: %w", err)
73 }
74
75 config := NewDefaultConfig()
76 config.APIUser = values[EnvUser]
77 config.APIKey = values[EnvAPIKey]
78
79 return NewDNSProviderConfig(config)
80 }
81
82 // NewDNSProviderConfig return a DNSProvider instance configured for Rackspace.
83 // It authenticates against the API, also grabbing the DNS Endpoint.
84 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
85 if config == nil {
86 return nil, errors.New("rackspace: the configuration of the DNS provider is nil")
87 }
88
89 if config.APIUser == "" || config.APIKey == "" {
90 return nil, errors.New("rackspace: credentials missing")
91 }
92
93 identifier := internal.NewIdentifier(config.HTTPClient, config.BaseURL)
94
95 identity, err := identifier.Login(context.Background(), config.APIUser, config.APIKey)
96 if err != nil {
97 return nil, fmt.Errorf("rackspace: %w", err)
98 }
99
100 // Iterate through the Service Catalog to get the DNS Endpoint
101 var dnsEndpoint string
102
103 for _, service := range identity.Access.ServiceCatalog {
104 if service.Name == "cloudDNS" {
105 dnsEndpoint = service.Endpoints[0].PublicURL
106 break
107 }
108 }
109
110 if dnsEndpoint == "" {
111 return nil, errors.New("rackspace: failed to populate DNS endpoint, check Rackspace API for changes")
112 }
113
114 client, err := internal.NewClient(dnsEndpoint, identity.Access.Token.ID)
115 if err != nil {
116 return nil, fmt.Errorf("rackspace: %w", err)
117 }
118
119 if config.HTTPClient != nil {
120 client.HTTPClient = config.HTTPClient
121 }
122
123 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
124
125 return &DNSProvider{
126 config: config,
127 client: client,
128 token: identity.Access.Token.ID,
129 cloudDNSEndpoint: dnsEndpoint,
130 }, nil
131 }
132
133 // Present creates a TXT record to fulfill the dns-01 challenge.
134 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
135 info := dns01.GetChallengeInfo(domain, keyAuth)
136
137 ctx := context.Background()
138
139 zoneID, err := d.client.GetHostedZoneID(ctx, info.EffectiveFQDN)
140 if err != nil {
141 return fmt.Errorf("rackspace: %w", err)
142 }
143
144 record := internal.Record{
145 Name: dns01.UnFqdn(info.EffectiveFQDN),
146 Type: "TXT",
147 Data: info.Value,
148 TTL: d.config.TTL,
149 }
150
151 err = d.client.AddRecord(ctx, zoneID, record)
152 if err != nil {
153 return fmt.Errorf("rackspace: %w", err)
154 }
155
156 return nil
157 }
158
159 // CleanUp removes the TXT record matching the specified parameters.
160 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
161 info := dns01.GetChallengeInfo(domain, keyAuth)
162
163 ctx := context.Background()
164
165 zoneID, err := d.client.GetHostedZoneID(ctx, info.EffectiveFQDN)
166 if err != nil {
167 return fmt.Errorf("rackspace: %w", err)
168 }
169
170 record, err := d.client.FindTxtRecord(ctx, info.EffectiveFQDN, zoneID)
171 if err != nil {
172 return fmt.Errorf("rackspace: %w", err)
173 }
174
175 err = d.client.DeleteRecord(ctx, zoneID, record.ID)
176 if err != nil {
177 return fmt.Errorf("rackspace: %w", err)
178 }
179
180 return nil
181 }
182
183 // Timeout returns the timeout and interval to use when checking for DNS propagation.
184 // Adjusting here to cope with spikes in propagation times.
185 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
186 return d.config.PropagationTimeout, d.config.PollingInterval
187 }
188