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 "strconv"
12 "time"
13
14 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
15 )
16
17 // defaultBaseURL represents the API endpoint to call.
18 const defaultBaseURL = "https://api.luadns.com"
19
20 // Client Lua DNS API client.
21 type Client struct {
22 apiUsername string
23 apiToken string
24
25 baseURL *url.URL
26 HTTPClient *http.Client
27 }
28
29 // NewClient creates a new Client.
30 func NewClient(apiUsername, apiToken string) *Client {
31 baseURL, _ := url.Parse(defaultBaseURL)
32
33 return &Client{
34 apiUsername: apiUsername,
35 apiToken: apiToken,
36 baseURL: baseURL,
37 HTTPClient: &http.Client{Timeout: 5 * time.Second},
38 }
39 }
40
41 // ListZones gets all the hosted zones.
42 // https://luadns.com/api.html#list-zones
43 func (c *Client) ListZones(ctx context.Context) ([]DNSZone, error) {
44 endpoint := c.baseURL.JoinPath("v1", "zones")
45
46 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
47 if err != nil {
48 return nil, err
49 }
50
51 var zones []DNSZone
52
53 err = c.do(req, &zones)
54 if err != nil {
55 return nil, fmt.Errorf("could not list zones: %w", err)
56 }
57
58 return zones, nil
59 }
60
61 // CreateRecord creates a new record in a zone.
62 // https://luadns.com/api.html#create-a-record
63 func (c *Client) CreateRecord(ctx context.Context, zone DNSZone, newRecord DNSRecord) (*DNSRecord, error) {
64 endpoint := c.baseURL.JoinPath("v1", "zones", strconv.Itoa(zone.ID), "records")
65
66 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, newRecord)
67 if err != nil {
68 return nil, err
69 }
70
71 var record *DNSRecord
72
73 err = c.do(req, &record)
74 if err != nil {
75 return nil, fmt.Errorf("could not create record %#v: %w", record, err)
76 }
77
78 return record, nil
79 }
80
81 // DeleteRecord deletes a record.
82 // https://luadns.com/api.html#delete-a-record
83 func (c *Client) DeleteRecord(ctx context.Context, record *DNSRecord) error {
84 endpoint := c.baseURL.JoinPath("v1", "zones", strconv.Itoa(record.ZoneID), "records", strconv.Itoa(record.ID))
85
86 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, record)
87 if err != nil {
88 return err
89 }
90
91 err = c.do(req, nil)
92 if err != nil {
93 return fmt.Errorf("could not delete record %#v: %w", record, err)
94 }
95
96 return nil
97 }
98
99 func (c *Client) do(req *http.Request, result any) error {
100 req.SetBasicAuth(c.apiUsername, c.apiToken)
101
102 resp, err := c.HTTPClient.Do(req)
103 if err != nil {
104 return errutils.NewHTTPDoError(req, err)
105 }
106
107 defer func() { _ = resp.Body.Close() }()
108
109 if resp.StatusCode != http.StatusOK {
110 return parseError(req, resp)
111 }
112
113 if result == nil {
114 return nil
115 }
116
117 raw, err := io.ReadAll(resp.Body)
118 if err != nil {
119 return errutils.NewReadResponseError(req, resp.StatusCode, err)
120 }
121
122 err = json.Unmarshal(raw, result)
123 if err != nil {
124 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
125 }
126
127 return nil
128 }
129
130 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
131 buf := new(bytes.Buffer)
132
133 if payload != nil {
134 err := json.NewEncoder(buf).Encode(payload)
135 if err != nil {
136 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
137 }
138 }
139
140 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
141 if err != nil {
142 return nil, fmt.Errorf("unable to create request: %w", err)
143 }
144
145 req.Header.Set("Accept", "application/json")
146
147 if payload != nil {
148 req.Header.Set("Content-Type", "application/json")
149 }
150
151 return req, nil
152 }
153
154 func parseError(req *http.Request, resp *http.Response) error {
155 raw, _ := io.ReadAll(resp.Body)
156
157 var errResp errorResponse
158
159 err := json.Unmarshal(raw, &errResp)
160 if err != nil {
161 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
162 }
163
164 return fmt.Errorf("status=%d: %w", resp.StatusCode, errResp)
165 }
166