ispconfig.go raw
1 // Package ispconfig implements a DNS provider for solving the DNS-01 challenge using ISPConfig.
2 package ispconfig
3
4 import (
5 "context"
6 "crypto/tls"
7 "errors"
8 "fmt"
9 "net/http"
10 "strconv"
11 "sync"
12 "time"
13
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/ispconfig/internal"
18 )
19
20 // Environment variables names.
21 const (
22 envNamespace = "ISPCONFIG_"
23
24 EnvServerURL = envNamespace + "SERVER_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 EnvInsecureSkipVerify = envNamespace + "INSECURE_SKIP_VERIFY"
33 )
34
35 // Config is used to configure the creation of the DNSProvider.
36 type Config struct {
37 ServerURL string
38 Username string
39 Password string
40
41 PropagationTimeout time.Duration
42 PollingInterval time.Duration
43 TTL int
44 HTTPClient *http.Client
45 InsecureSkipVerify bool
46 }
47
48 // NewDefaultConfig returns a default configuration for the DNSProvider.
49 func NewDefaultConfig() *Config {
50 return &Config{
51 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
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]string
66 recordIDsMu sync.Mutex
67 }
68
69 // NewDNSProvider returns a DNSProvider instance configured for ISPConfig.
70 func NewDNSProvider() (*DNSProvider, error) {
71 values, err := env.Get(EnvServerURL, EnvUsername, EnvPassword)
72 if err != nil {
73 return nil, fmt.Errorf("ispconfig: %w", err)
74 }
75
76 config := NewDefaultConfig()
77 config.ServerURL = values[EnvServerURL]
78 config.Username = values[EnvUsername]
79 config.Password = values[EnvPassword]
80 config.InsecureSkipVerify = env.GetOrDefaultBool(EnvInsecureSkipVerify, false)
81
82 return NewDNSProviderConfig(config)
83 }
84
85 // NewDNSProviderConfig return a DNSProvider instance configured for ISPConfig.
86 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
87 if config == nil {
88 return nil, errors.New("ispconfig: the configuration of the DNS provider is nil")
89 }
90
91 if config.ServerURL == "" {
92 return nil, errors.New("ispconfig: missing server URL")
93 }
94
95 if config.Username == "" || config.Password == "" {
96 return nil, errors.New("ispconfig: credentials missing")
97 }
98
99 client, err := internal.NewClient(config.ServerURL)
100 if err != nil {
101 return nil, fmt.Errorf("ispconfig: %w", err)
102 }
103
104 if config.HTTPClient != nil {
105 client.HTTPClient = config.HTTPClient
106 }
107
108 if config.InsecureSkipVerify {
109 client.HTTPClient.Transport = &http.Transport{
110 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
111 }
112 }
113
114 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
115
116 return &DNSProvider{
117 config: config,
118 client: client,
119 recordIDs: make(map[string]string),
120 }, nil
121 }
122
123 // Present creates a TXT record using the specified parameters.
124 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
125 ctx := context.Background()
126
127 info := dns01.GetChallengeInfo(domain, keyAuth)
128
129 sessionID, err := d.client.Login(ctx, d.config.Username, d.config.Password)
130 if err != nil {
131 return fmt.Errorf("ispconfig: login: %w", err)
132 }
133
134 zoneID, err := d.findZone(ctx, sessionID, info.EffectiveFQDN)
135 if err != nil {
136 return fmt.Errorf("ispconfig: get zone id: %w", err)
137 }
138
139 zone, err := d.client.GetZone(ctx, sessionID, strconv.Itoa(zoneID))
140 if err != nil {
141 return fmt.Errorf("ispconfig: get zone: %w", err)
142 }
143
144 clientID, err := d.client.GetClientID(ctx, sessionID, zone.SysUserID)
145 if err != nil {
146 return fmt.Errorf("ispconfig: get client id: %w", err)
147 }
148
149 params := internal.RecordParams{
150 ServerID: "serverA",
151 Zone: zone.ID,
152 Name: info.EffectiveFQDN,
153 Type: "txt",
154 Data: info.Value,
155 Aux: "0",
156 TTL: strconv.Itoa(d.config.TTL),
157 Active: "y",
158 Stamp: time.Now().UTC().Format("2006-01-02 15:04:05"),
159 }
160
161 recordID, err := d.client.AddTXT(ctx, sessionID, strconv.Itoa(clientID), params)
162 if err != nil {
163 return fmt.Errorf("ispconfig: add txt record: %w", err)
164 }
165
166 d.recordIDsMu.Lock()
167 d.recordIDs[token] = recordID
168 d.recordIDsMu.Unlock()
169
170 return nil
171 }
172
173 // CleanUp removes the TXT record matching the specified parameters.
174 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
175 ctx := context.Background()
176
177 info := dns01.GetChallengeInfo(domain, keyAuth)
178
179 // gets the record's unique ID
180 d.recordIDsMu.Lock()
181 recordID, ok := d.recordIDs[token]
182 d.recordIDsMu.Unlock()
183
184 if !ok {
185 return fmt.Errorf("ispconfig: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token)
186 }
187
188 sessionID, err := d.client.Login(ctx, d.config.Username, d.config.Password)
189 if err != nil {
190 return fmt.Errorf("ispconfig: login: %w", err)
191 }
192
193 _, err = d.client.DeleteTXT(ctx, sessionID, recordID)
194 if err != nil {
195 return fmt.Errorf("ispconfig: delete txt record: %w", err)
196 }
197
198 d.recordIDsMu.Lock()
199 delete(d.recordIDs, token)
200 d.recordIDsMu.Unlock()
201
202 return nil
203 }
204
205 // Timeout returns the timeout and interval to use when checking for DNS propagation.
206 // Adjusting here to cope with spikes in propagation times.
207 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
208 return d.config.PropagationTimeout, d.config.PollingInterval
209 }
210
211 func (d *DNSProvider) findZone(ctx context.Context, sessionID, fqdn string) (int, error) {
212 for domain := range dns01.UnFqdnDomainsSeq(fqdn) {
213 zoneID, err := d.client.GetZoneID(ctx, sessionID, domain)
214 if err == nil {
215 return zoneID, nil
216 }
217 }
218
219 return 0, fmt.Errorf("zone not found for %q", fqdn)
220 }
221