client.go raw
1 package internal
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "net/http"
9 "net/url"
10 "strings"
11 "sync"
12 "time"
13
14 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
15 )
16
17 const (
18 removeAction = "rm"
19 addAction = "add"
20 )
21
22 const successCode = "successfully"
23
24 const defaultBaseURL = "https://www.dnshome.de/dyndns.php"
25
26 // Client the dnsHome.de client.
27 type Client struct {
28 baseURL string
29 HTTPClient *http.Client
30
31 credentials map[string]string
32 credMu sync.Mutex
33 }
34
35 // NewClient Creates a new Client.
36 func NewClient(credentials map[string]string) *Client {
37 return &Client{
38 HTTPClient: &http.Client{Timeout: 10 * time.Second},
39 baseURL: defaultBaseURL,
40 credentials: credentials,
41 }
42 }
43
44 // Add adds a TXT record.
45 // only one TXT record for ACME is allowed, so it will update the "current" TXT record.
46 func (c *Client) Add(ctx context.Context, hostname, value string) error {
47 domain := strings.TrimPrefix(hostname, "_acme-challenge.")
48
49 return c.doAction(ctx, domain, addAction, value)
50 }
51
52 // Remove removes a TXT record.
53 // only one TXT record for ACME is allowed, so it will remove "all" the TXT records.
54 func (c *Client) Remove(ctx context.Context, hostname, value string) error {
55 domain := strings.TrimPrefix(hostname, "_acme-challenge.")
56
57 return c.doAction(ctx, domain, removeAction, value)
58 }
59
60 func (c *Client) doAction(ctx context.Context, domain, action, value string) error {
61 endpoint, err := c.createEndpoint(domain, action, value)
62 if err != nil {
63 return err
64 }
65
66 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), http.NoBody)
67 if err != nil {
68 return fmt.Errorf("unable to create request: %w", err)
69 }
70
71 resp, err := c.HTTPClient.Do(req)
72 if err != nil {
73 return errutils.NewHTTPDoError(req, err)
74 }
75
76 defer func() { _ = resp.Body.Close() }()
77
78 if resp.StatusCode != http.StatusOK {
79 return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
80 }
81
82 raw, err := io.ReadAll(resp.Body)
83 if err != nil {
84 return errutils.NewReadResponseError(req, resp.StatusCode, err)
85 }
86
87 output := string(raw)
88
89 if !strings.HasPrefix(output, successCode) {
90 return errors.New(output)
91 }
92
93 return nil
94 }
95
96 func (c *Client) createEndpoint(domain, action, value string) (*url.URL, error) {
97 if len(value) < 12 {
98 return nil, fmt.Errorf("the TXT value must have more than 12 characters: %s", value)
99 }
100
101 endpoint, err := url.Parse(c.baseURL)
102 if err != nil {
103 return nil, err
104 }
105
106 c.credMu.Lock()
107 password, ok := c.credentials[domain]
108 c.credMu.Unlock()
109
110 if !ok {
111 return nil, fmt.Errorf("domain %s not found in credentials, check your credentials map", domain)
112 }
113
114 endpoint.User = url.UserPassword(domain, password)
115
116 query := endpoint.Query()
117 query.Set("acme", action)
118 query.Set("txt", value)
119 endpoint.RawQuery = query.Encode()
120
121 return endpoint, nil
122 }
123