client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "net/http"
11 "net/url"
12 "strconv"
13 "time"
14
15 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
16 )
17
18 // APIKeyHeader API key header.
19 const APIKeyHeader = "X-Api-Key"
20
21 // Client the KeyHelp API client.
22 type Client struct {
23 apiKey string
24
25 baseURL *url.URL
26 HTTPClient *http.Client
27 }
28
29 // NewClient creates a new Client.
30 func NewClient(baseURL, apiKey string) (*Client, error) {
31 if baseURL == "" {
32 return nil, errors.New("missing base URL")
33 }
34
35 if apiKey == "" {
36 return nil, errors.New("credentials missing")
37 }
38
39 base, err := url.Parse(baseURL)
40 if err != nil {
41 return nil, fmt.Errorf("parse base URL: %w", err)
42 }
43
44 return &Client{
45 apiKey: apiKey,
46 baseURL: base.JoinPath("api", "v2"),
47 HTTPClient: &http.Client{Timeout: 10 * time.Second},
48 }, nil
49 }
50
51 func (c *Client) do(req *http.Request, result any) error {
52 req.Header.Set(APIKeyHeader, c.apiKey)
53
54 resp, err := c.HTTPClient.Do(req)
55 if err != nil {
56 return errutils.NewHTTPDoError(req, err)
57 }
58
59 defer func() { _ = resp.Body.Close() }()
60
61 if resp.StatusCode/100 != 2 {
62 return parseError(req, resp)
63 }
64
65 if result == nil {
66 return nil
67 }
68
69 raw, err := io.ReadAll(resp.Body)
70 if err != nil {
71 return errutils.NewReadResponseError(req, resp.StatusCode, err)
72 }
73
74 err = json.Unmarshal(raw, result)
75 if err != nil {
76 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
77 }
78
79 return nil
80 }
81
82 func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {
83 endpoint := c.baseURL.JoinPath("domains")
84
85 query := endpoint.Query()
86 query.Set("sort", "domain_utf8")
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 var result []Domain
95
96 err = c.do(req, &result)
97 if err != nil {
98 return nil, err
99 }
100
101 return result, nil
102 }
103
104 func (c *Client) ListDomainRecords(ctx context.Context, domainID int) (*DomainRecords, error) {
105 endpoint := c.baseURL.JoinPath("dns", strconv.Itoa(domainID))
106
107 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
108 if err != nil {
109 return nil, err
110 }
111
112 var result DomainRecords
113
114 err = c.do(req, &result)
115 if err != nil {
116 return nil, err
117 }
118
119 return &result, nil
120 }
121
122 func (c *Client) UpdateDomainRecords(ctx context.Context, domainID int, records DomainRecords) (*DomainID, error) {
123 endpoint := c.baseURL.JoinPath("dns", strconv.Itoa(domainID))
124
125 req, err := newJSONRequest(ctx, http.MethodPut, endpoint, records)
126 if err != nil {
127 return nil, err
128 }
129
130 var result DomainID
131
132 err = c.do(req, &result)
133 if err != nil {
134 return nil, err
135 }
136
137 return &result, nil
138 }
139
140 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
141 buf := new(bytes.Buffer)
142
143 if payload != nil {
144 err := json.NewEncoder(buf).Encode(payload)
145 if err != nil {
146 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
147 }
148 }
149
150 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
151 if err != nil {
152 return nil, fmt.Errorf("unable to create request: %w", err)
153 }
154
155 req.Header.Set("Accept", "application/json")
156
157 if payload != nil {
158 req.Header.Set("Content-Type", "application/json")
159 }
160
161 return req, nil
162 }
163
164 func parseError(req *http.Request, resp *http.Response) error {
165 raw, _ := io.ReadAll(resp.Body)
166
167 var errAPI APIError
168
169 err := json.Unmarshal(raw, &errAPI)
170 if err != nil {
171 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
172 }
173
174 return &errAPI
175 }
176