client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "encoding/xml"
6 "errors"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "slices"
12 "time"
13
14 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
15 )
16
17 const defaultBaseURL = "https://dynamic.zoneedit.com"
18
19 // Client the ZoneEdit API client.
20 type Client struct {
21 user string
22 authToken string
23
24 baseURL *url.URL
25 HTTPClient *http.Client
26 }
27
28 // NewClient creates a new Client.
29 func NewClient(user, authToken string) (*Client, error) {
30 if user == "" || authToken == "" {
31 return nil, errors.New("credentials missing")
32 }
33
34 baseURL, _ := url.Parse(defaultBaseURL)
35
36 return &Client{
37 user: user,
38 authToken: authToken,
39 baseURL: baseURL,
40 HTTPClient: &http.Client{Timeout: 10 * time.Second},
41 }, nil
42 }
43
44 func (c *Client) CreateTXTRecord(domain, rdata string) error {
45 return c.perform("txt-create.php", domain, rdata)
46 }
47
48 func (c *Client) DeleteTXTRecord(domain, rdata string) error {
49 return c.perform("txt-delete.php", domain, rdata)
50 }
51
52 func (c *Client) perform(actionPath, domain, rdata string) error {
53 endpoint := c.baseURL.JoinPath(actionPath)
54
55 query := endpoint.Query()
56 query.Set("host", domain)
57 query.Set("rdata", rdata)
58 endpoint.RawQuery = query.Encode()
59
60 req, err := http.NewRequest(http.MethodGet, endpoint.String(), http.NoBody)
61 if err != nil {
62 return err
63 }
64
65 return c.do(req)
66 }
67
68 func (c *Client) do(req *http.Request) error {
69 req.SetBasicAuth(c.user, c.authToken)
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/100 != 2 {
79 raw, _ := io.ReadAll(resp.Body)
80
81 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
82 }
83
84 raw, err := io.ReadAll(resp.Body)
85 if err != nil {
86 return errutils.NewReadResponseError(req, resp.StatusCode, err)
87 }
88
89 if bytes.Contains(raw, []byte("SUCCESS CODE")) {
90 return nil
91 }
92
93 raw = bytes.TrimSpace(raw)
94
95 // The answer is not an XML valid (missing closing), so I fix it to parse it.
96 if bytes.HasSuffix(raw, []byte(">")) {
97 raw = slices.Concat(raw[:len(raw)-1], []byte("/>"))
98 }
99
100 var apiErr APIError
101
102 err = xml.Unmarshal(raw, &apiErr)
103 if err != nil {
104 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
105 }
106
107 return fmt.Errorf("[status code: %d] %w", resp.StatusCode, apiErr)
108 }
109