client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "time"
12
13 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
14 )
15
16 type Client struct {
17 serverURL string
18 HTTPClient *http.Client
19 }
20
21 func NewClient(serverURL string) (*Client, error) {
22 _, err := url.Parse(serverURL)
23 if err != nil {
24 return nil, fmt.Errorf("server URL: %w", err)
25 }
26
27 return &Client{
28 serverURL: serverURL,
29 HTTPClient: &http.Client{Timeout: 10 * time.Second},
30 }, nil
31 }
32
33 func (c *Client) Login(ctx context.Context, username, password string) (string, error) {
34 payload := LoginRequest{
35 Username: username,
36 Password: password,
37 ClientLogin: false,
38 }
39
40 endpoint, err := url.Parse(c.serverURL)
41 if err != nil {
42 return "", err
43 }
44
45 endpoint.RawQuery = "login"
46
47 req, err := newJSONRequest(ctx, endpoint, payload)
48 if err != nil {
49 return "", err
50 }
51
52 var response APIResponse
53
54 err = c.do(req, &response)
55 if err != nil {
56 return "", err
57 }
58
59 return extractResponse[string](response)
60 }
61
62 func (c *Client) GetClientID(ctx context.Context, sessionID, sysUserID string) (int, error) {
63 payload := ClientIDRequest{
64 SessionID: sessionID,
65 SysUserID: sysUserID,
66 }
67
68 endpoint, err := url.Parse(c.serverURL)
69 if err != nil {
70 return 0, err
71 }
72
73 endpoint.RawQuery = "client_get_id"
74
75 req, err := newJSONRequest(ctx, endpoint, payload)
76 if err != nil {
77 return 0, err
78 }
79
80 var response APIResponse
81
82 err = c.do(req, &response)
83 if err != nil {
84 return 0, err
85 }
86
87 return extractResponse[int](response)
88 }
89
90 // GetZoneID returns the zone ID for the given name.
91 func (c *Client) GetZoneID(ctx context.Context, sessionID, name string) (int, error) {
92 payload := map[string]any{
93 "session_id": sessionID,
94 "origin": name,
95 }
96
97 endpoint, err := url.Parse(c.serverURL)
98 if err != nil {
99 return 0, err
100 }
101
102 endpoint.RawQuery = "dns_zone_get_id"
103
104 req, err := newJSONRequest(ctx, endpoint, payload)
105 if err != nil {
106 return 0, err
107 }
108
109 var response APIResponse
110
111 err = c.do(req, &response)
112 if err != nil {
113 return 0, err
114 }
115
116 return extractResponse[int](response)
117 }
118
119 // GetZone returns the zone information for the zone ID.
120 func (c *Client) GetZone(ctx context.Context, sessionID, zoneID string) (*Zone, error) {
121 payload := map[string]any{
122 "session_id": sessionID,
123 "primary_id": zoneID,
124 }
125
126 endpoint, err := url.Parse(c.serverURL)
127 if err != nil {
128 return nil, err
129 }
130
131 endpoint.RawQuery = "dns_zone_get"
132
133 req, err := newJSONRequest(ctx, endpoint, payload)
134 if err != nil {
135 return nil, err
136 }
137
138 var response APIResponse
139
140 err = c.do(req, &response)
141 if err != nil {
142 return nil, err
143 }
144
145 return extractResponse[*Zone](response)
146 }
147
148 // GetTXT returns the TXT record for the given name.
149 // `name` must be a fully qualified domain name, e.g. "example.com.".
150 func (c *Client) GetTXT(ctx context.Context, sessionID, name string) (*Record, error) {
151 payload := GetTXTRequest{
152 SessionID: sessionID,
153 PrimaryID: struct {
154 Name string `json:"name"`
155 Type string `json:"type"`
156 }{
157 Name: name,
158 Type: "txt",
159 },
160 }
161
162 endpoint, err := url.Parse(c.serverURL)
163 if err != nil {
164 return nil, err
165 }
166
167 endpoint.RawQuery = "dns_txt_get"
168
169 req, err := newJSONRequest(ctx, endpoint, payload)
170 if err != nil {
171 return nil, err
172 }
173
174 var response APIResponse
175
176 err = c.do(req, &response)
177 if err != nil {
178 return nil, err
179 }
180
181 return extractResponse[*Record](response)
182 }
183
184 // AddTXT adds a TXT record.
185 // It returns the ID of the newly created record.
186 func (c *Client) AddTXT(ctx context.Context, sessionID, clientID string, params RecordParams) (string, error) {
187 payload := AddTXTRequest{
188 SessionID: sessionID,
189 ClientID: clientID,
190 Params: ¶ms,
191 UpdateSerial: true,
192 }
193
194 endpoint, err := url.Parse(c.serverURL)
195 if err != nil {
196 return "", err
197 }
198
199 endpoint.RawQuery = "dns_txt_add"
200
201 req, err := newJSONRequest(ctx, endpoint, payload)
202 if err != nil {
203 return "", err
204 }
205
206 var response APIResponse
207
208 err = c.do(req, &response)
209 if err != nil {
210 return "", err
211 }
212
213 return extractResponse[string](response)
214 }
215
216 // DeleteTXT deletes a TXT record.
217 // It returns the number of deleted records.
218 func (c *Client) DeleteTXT(ctx context.Context, sessionID, recordID string) (int, error) {
219 payload := DeleteTXTRequest{
220 SessionID: sessionID,
221 PrimaryID: recordID,
222 UpdateSerial: true,
223 }
224
225 endpoint, err := url.Parse(c.serverURL)
226 if err != nil {
227 return 0, err
228 }
229
230 endpoint.RawQuery = "dns_txt_delete"
231
232 req, err := newJSONRequest(ctx, endpoint, payload)
233 if err != nil {
234 return 0, err
235 }
236
237 var response APIResponse
238
239 err = c.do(req, &response)
240 if err != nil {
241 return 0, err
242 }
243
244 return extractResponse[int](response)
245 }
246
247 func (c *Client) do(req *http.Request, result any) error {
248 resp, err := c.HTTPClient.Do(req)
249 if err != nil {
250 return errutils.NewHTTPDoError(req, err)
251 }
252
253 defer func() { _ = resp.Body.Close() }()
254
255 if resp.StatusCode/100 != 2 {
256 raw, _ := io.ReadAll(resp.Body)
257
258 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
259 }
260
261 if result == nil {
262 return nil
263 }
264
265 raw, err := io.ReadAll(resp.Body)
266 if err != nil {
267 return errutils.NewReadResponseError(req, resp.StatusCode, err)
268 }
269
270 err = json.Unmarshal(raw, result)
271 if err != nil {
272 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
273 }
274
275 return nil
276 }
277
278 func newJSONRequest(ctx context.Context, endpoint *url.URL, payload any) (*http.Request, error) {
279 buf := new(bytes.Buffer)
280
281 if payload != nil {
282 err := json.NewEncoder(buf).Encode(payload)
283 if err != nil {
284 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
285 }
286 }
287
288 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), buf)
289 if err != nil {
290 return nil, fmt.Errorf("unable to create request: %w", err)
291 }
292
293 req.Header.Set("Accept", "application/json")
294
295 if payload != nil {
296 req.Header.Set("Content-Type", "application/json")
297 }
298
299 return req, nil
300 }
301
302 func extractResponse[T any](response APIResponse) (T, error) {
303 if response.Code != "ok" {
304 var zero T
305
306 return zero, &APIError{APIResponse: response}
307 }
308
309 var result T
310
311 err := json.Unmarshal(response.Response, &result)
312 if err != nil {
313 var zero T
314 return zero, fmt.Errorf("unable to unmarshal response: %s, %w", string(response.Response), err)
315 }
316
317 return result, nil
318 }
319