plesk.go raw
1 // Package plesk implements a DNS provider for solving the DNS-01 challenge using Plesk DNS.
2 package plesk
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "net/url"
10 "sync"
11 "time"
12
13 "github.com/go-acme/lego/v4/challenge"
14 "github.com/go-acme/lego/v4/challenge/dns01"
15 "github.com/go-acme/lego/v4/platform/config/env"
16 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
17 "github.com/go-acme/lego/v4/providers/dns/plesk/internal"
18 )
19
20 // Environment variables names.
21 const (
22 envNamespace = "PLESK_"
23
24 EnvServerBaseURL = envNamespace + "SERVER_BASE_URL"
25 EnvUsername = envNamespace + "USERNAME"
26 EnvPassword = envNamespace + "PASSWORD"
27
28 EnvTTL = envNamespace + "TTL"
29 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
30 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
31 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
32 )
33
34 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
35
36 // Config is used to configure the creation of the DNSProvider.
37 type Config struct {
38 baseURL string
39 Username string
40 Password string
41
42 PropagationTimeout time.Duration
43 PollingInterval time.Duration
44 TTL int
45 HTTPClient *http.Client
46 }
47
48 // NewDefaultConfig returns a default configuration for the DNSProvider.
49 func NewDefaultConfig() *Config {
50 return &Config{
51 TTL: env.GetOrDefaultInt(EnvTTL, 300),
52 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
53 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
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]int
66 recordIDsMu sync.Mutex
67 }
68
69 // NewDNSProvider returns a DNSProvider instance configured for Plesk.
70 // Credentials must be passed in the environment variables:
71 // PLESK_USERNAME and PLESK_PASSWORD.
72 func NewDNSProvider() (*DNSProvider, error) {
73 values, err := env.Get(EnvServerBaseURL, EnvUsername, EnvPassword)
74 if err != nil {
75 return nil, fmt.Errorf("plesk: %w", err)
76 }
77
78 config := NewDefaultConfig()
79 config.baseURL = values[EnvServerBaseURL]
80 config.Username = values[EnvUsername]
81 config.Password = values[EnvPassword]
82
83 return NewDNSProviderConfig(config)
84 }
85
86 // NewDNSProviderConfig return a DNSProvider instance configured for Plesk.
87 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
88 if config == nil {
89 return nil, errors.New("plesk: the configuration of the DNS provider is nil")
90 }
91
92 if config.baseURL == "" {
93 return nil, errors.New("plesk: missing server base URL")
94 }
95
96 baseURL, err := url.Parse(config.baseURL)
97 if err != nil {
98 return nil, fmt.Errorf("plesk: failed to parse base URL (%s): %w", config.baseURL, err)
99 }
100
101 if config.Username == "" || config.Password == "" {
102 return nil, errors.New("plesk: incomplete credentials, missing username and/or password")
103 }
104
105 client := internal.NewClient(baseURL, config.Username, config.Password)
106
107 if config.HTTPClient != nil {
108 client.HTTPClient = config.HTTPClient
109 }
110
111 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
112
113 return &DNSProvider{
114 config: config,
115 client: client,
116 recordIDs: map[string]int{},
117 }, nil
118 }
119
120 // Timeout returns the timeout and interval to use when checking for DNS propagation.
121 // Adjusting here to cope with spikes in propagation times.
122 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
123 return d.config.PropagationTimeout, d.config.PollingInterval
124 }
125
126 // Present creates a TXT record using the specified parameters.
127 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
128 info := dns01.GetChallengeInfo(domain, keyAuth)
129
130 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
131 if err != nil {
132 return fmt.Errorf("plesk: could not find zone for domain %q: %w", domain, err)
133 }
134
135 ctx := context.Background()
136
137 siteID, err := d.client.GetSite(ctx, dns01.UnFqdn(authZone))
138 if err != nil {
139 return fmt.Errorf("plesk: failed to get site: %w", err)
140 }
141
142 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
143 if err != nil {
144 return fmt.Errorf("nodion: %w", err)
145 }
146
147 recordID, err := d.client.AddRecord(ctx, siteID, subDomain, info.Value)
148 if err != nil {
149 return fmt.Errorf("plesk: failed to add record: %w", err)
150 }
151
152 d.recordIDsMu.Lock()
153 d.recordIDs[token] = recordID
154 d.recordIDsMu.Unlock()
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 d.recordIDsMu.Lock()
164 recordID, ok := d.recordIDs[token]
165 d.recordIDsMu.Unlock()
166
167 if !ok {
168 return fmt.Errorf("plesk: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token)
169 }
170
171 _, err := d.client.DeleteRecord(context.Background(), recordID)
172 if err != nil {
173 return fmt.Errorf("plesk: failed to delete record (%d): %w", recordID, err)
174 }
175
176 d.recordIDsMu.Lock()
177 delete(d.recordIDs, token)
178 d.recordIDsMu.Unlock()
179
180 return nil
181 }
182