client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "crypto/hmac"
7 "crypto/sha1"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "io"
13 "net/http"
14 "net/url"
15 "strconv"
16 "time"
17
18 "github.com/go-acme/lego/v4/challenge/dns01"
19 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
20 )
21
22 // Default API endpoints.
23 const (
24 DefaultSandboxBaseURL = "https://api.sandbox.dnsmadeeasy.com/V2.0"
25 DefaultProdBaseURL = "https://api.dnsmadeeasy.com/V2.0"
26 )
27
28 // Client DNSMadeEasy client.
29 type Client struct {
30 apiKey string
31 apiSecret string
32
33 BaseURL *url.URL
34 HTTPClient *http.Client
35 }
36
37 // NewClient creates a DNSMadeEasy client.
38 func NewClient(apiKey, apiSecret string) (*Client, error) {
39 if apiKey == "" {
40 return nil, errors.New("credentials missing: API key")
41 }
42
43 if apiSecret == "" {
44 return nil, errors.New("credentials missing: API secret")
45 }
46
47 baseURL, _ := url.Parse(DefaultProdBaseURL)
48
49 return &Client{
50 apiKey: apiKey,
51 apiSecret: apiSecret,
52 BaseURL: baseURL,
53 HTTPClient: &http.Client{Timeout: 5 * time.Second},
54 }, nil
55 }
56
57 // GetDomain gets a domain.
58 func (c *Client) GetDomain(ctx context.Context, authZone string) (*Domain, error) {
59 endpoint := c.BaseURL.JoinPath("dns", "managed", "name")
60
61 query := endpoint.Query()
62 query.Set("domainname", dns01.UnFqdn(authZone))
63 endpoint.RawQuery = query.Encode()
64
65 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
66 if err != nil {
67 return nil, err
68 }
69
70 domain := &Domain{}
71
72 err = c.do(req, domain)
73 if err != nil {
74 return nil, err
75 }
76
77 return domain, nil
78 }
79
80 // GetRecords gets all TXT records.
81 func (c *Client) GetRecords(ctx context.Context, domain *Domain, recordName, recordType string) (*[]Record, error) {
82 endpoint := c.BaseURL.JoinPath("dns", "managed", strconv.Itoa(domain.ID), "records")
83
84 query := endpoint.Query()
85 query.Set("recordName", recordName)
86 query.Set("type", recordType)
87 endpoint.RawQuery = query.Encode()
88
89 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
90 if err != nil {
91 return nil, err
92 }
93
94 records := &recordsResponse{}
95
96 err = c.do(req, records)
97 if err != nil {
98 return nil, err
99 }
100
101 return records.Records, nil
102 }
103
104 // CreateRecord creates a TXT records.
105 func (c *Client) CreateRecord(ctx context.Context, domain *Domain, record *Record) error {
106 endpoint := c.BaseURL.JoinPath("dns", "managed", strconv.Itoa(domain.ID), "records")
107
108 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
109 if err != nil {
110 return err
111 }
112
113 return c.do(req, nil)
114 }
115
116 // DeleteRecord deletes a TXT records.
117 func (c *Client) DeleteRecord(ctx context.Context, record Record) error {
118 endpoint := c.BaseURL.JoinPath("dns", "managed", strconv.Itoa(record.SourceID), "records", strconv.Itoa(record.ID))
119
120 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
121 if err != nil {
122 return err
123 }
124
125 return c.do(req, nil)
126 }
127
128 func (c *Client) do(req *http.Request, result any) error {
129 err := c.sign(req, time.Now().UTC().Format(time.RFC1123))
130 if err != nil {
131 return err
132 }
133
134 resp, err := c.HTTPClient.Do(req)
135 if err != nil {
136 return errutils.NewHTTPDoError(req, err)
137 }
138
139 defer func() { _ = resp.Body.Close() }()
140
141 if resp.StatusCode/100 != 2 {
142 return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
143 }
144
145 if result == nil {
146 return nil
147 }
148
149 raw, err := io.ReadAll(resp.Body)
150 if err != nil {
151 return errutils.NewReadResponseError(req, resp.StatusCode, err)
152 }
153
154 if err = json.Unmarshal(raw, result); err != nil {
155 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
156 }
157
158 return nil
159 }
160
161 func (c *Client) sign(req *http.Request, timestamp string) error {
162 signature, err := computeHMAC(timestamp, c.apiSecret)
163 if err != nil {
164 return err
165 }
166
167 req.Header.Set("x-dnsme-apiKey", c.apiKey)
168 req.Header.Set("x-dnsme-requestDate", timestamp)
169 req.Header.Set("x-dnsme-hmac", signature)
170
171 return nil
172 }
173
174 func computeHMAC(message, secret string) (string, error) {
175 key := []byte(secret)
176 h := hmac.New(sha1.New, key)
177
178 _, err := h.Write([]byte(message))
179 if err != nil {
180 return "", err
181 }
182
183 return hex.EncodeToString(h.Sum(nil)), nil
184 }
185
186 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
187 buf := new(bytes.Buffer)
188
189 if payload != nil {
190 err := json.NewEncoder(buf).Encode(payload)
191 if err != nil {
192 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
193 }
194 }
195
196 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
197 if err != nil {
198 return nil, fmt.Errorf("unable to create request: %w", err)
199 }
200
201 req.Header.Set("Accept", "application/json")
202
203 if payload != nil {
204 req.Header.Set("Content-Type", "application/json")
205 }
206
207 return req, nil
208 }
209