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 querystring "github.com/google/go-querystring/query"
15 )
16
17 const defaultBaseURL = "https://api.derak.cloud/v1.0"
18
19 type Client struct {
20 baseURL *url.URL
21 HTTPClient *http.Client
22 zoneEndpoint string
23
24 apiKey string
25 }
26
27 func NewClient(apiKey string) *Client {
28 baseURL, _ := url.Parse(defaultBaseURL)
29
30 return &Client{
31 HTTPClient: &http.Client{Timeout: 10 * time.Second},
32 baseURL: baseURL,
33 zoneEndpoint: "https://api.derak.cloud/api/v2/service/cdn/zones",
34 apiKey: apiKey,
35 }
36 }
37
38 // GetRecords gets all records.
39 // Note: the response is not influenced by the query parameters, so the documentation seems wrong.
40 func (c *Client) GetRecords(ctx context.Context, zoneID string, params *GetRecordsParameters) (*GetRecordsResponse, error) {
41 endpoint := c.baseURL.JoinPath("zones", zoneID, "dnsrecords")
42
43 v, err := querystring.Values(params)
44 if err != nil {
45 return nil, err
46 }
47
48 endpoint.RawQuery = v.Encode()
49
50 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
51 if err != nil {
52 return nil, err
53 }
54
55 response := &GetRecordsResponse{}
56
57 err = c.do(req, response)
58 if err != nil {
59 return nil, err
60 }
61
62 return response, nil
63 }
64
65 // GetRecord gets a record by ID.
66 func (c *Client) GetRecord(ctx context.Context, zoneID, recordID string) (*Record, error) {
67 endpoint := c.baseURL.JoinPath("zones", zoneID, "dnsrecords", recordID)
68
69 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
70 if err != nil {
71 return nil, err
72 }
73
74 response := &Record{}
75
76 err = c.do(req, response)
77 if err != nil {
78 return nil, err
79 }
80
81 return response, nil
82 }
83
84 // CreateRecord creates a new record.
85 func (c *Client) CreateRecord(ctx context.Context, zoneID string, record Record) (*Record, error) {
86 endpoint := c.baseURL.JoinPath("zones", zoneID, "dnsrecords")
87
88 req, err := newJSONRequest(ctx, http.MethodPut, endpoint, record)
89 if err != nil {
90 return nil, err
91 }
92
93 response := &Record{}
94
95 err = c.do(req, response)
96 if err != nil {
97 return nil, err
98 }
99
100 return response, nil
101 }
102
103 // EditRecord edits an existing record.
104 func (c *Client) EditRecord(ctx context.Context, zoneID, recordID string, record Record) (*Record, error) {
105 endpoint := c.baseURL.JoinPath("zones", zoneID, "dnsrecords", recordID)
106
107 req, err := newJSONRequest(ctx, http.MethodPatch, endpoint, record)
108 if err != nil {
109 return nil, err
110 }
111
112 response := &Record{}
113
114 err = c.do(req, response)
115 if err != nil {
116 return nil, err
117 }
118
119 return response, nil
120 }
121
122 // DeleteRecord deletes an existing record.
123 func (c *Client) DeleteRecord(ctx context.Context, zoneID, recordID string) error {
124 endpoint := c.baseURL.JoinPath("zones", zoneID, "dnsrecords", recordID)
125
126 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
127 if err != nil {
128 return err
129 }
130
131 response := &APIResponse[any]{}
132
133 err = c.do(req, response)
134 if err != nil {
135 return err
136 }
137
138 if !response.Success {
139 return fmt.Errorf("API error: %d %s", response.Error, codeText(response.Error))
140 }
141
142 return nil
143 }
144
145 // GetZones gets zones.
146 // Note: it's not a part of the official API, there is no documentation about this.
147 // The endpoint comes from UI calls analysis.
148 func (c *Client) GetZones(ctx context.Context) ([]Zone, error) {
149 req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.zoneEndpoint, http.NoBody)
150 if err != nil {
151 return nil, err
152 }
153
154 response := &APIResponse[[]Zone]{}
155
156 err = c.do(req, response)
157 if err != nil {
158 return nil, err
159 }
160
161 if !response.Success {
162 return nil, fmt.Errorf("API error: %d %s", response.Error, codeText(response.Error))
163 }
164
165 return response.Result, nil
166 }
167
168 func (c *Client) do(req *http.Request, result any) error {
169 req.SetBasicAuth("api", c.apiKey)
170
171 resp, err := c.HTTPClient.Do(req)
172 if err != nil {
173 return errutils.NewHTTPDoError(req, err)
174 }
175
176 defer func() { _ = resp.Body.Close() }()
177
178 switch req.Method {
179 case http.MethodPut:
180 if resp.StatusCode != http.StatusCreated {
181 return parseError(req, resp)
182 }
183 default:
184 if resp.StatusCode != http.StatusOK {
185 return parseError(req, resp)
186 }
187 }
188
189 raw, err := io.ReadAll(resp.Body)
190 if err != nil {
191 return errutils.NewReadResponseError(req, resp.StatusCode, err)
192 }
193
194 err = json.Unmarshal(raw, result)
195 if err != nil {
196 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
197 }
198
199 return nil
200 }
201
202 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
203 buf := new(bytes.Buffer)
204
205 if payload != nil {
206 err := json.NewEncoder(buf).Encode(payload)
207 if err != nil {
208 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
209 }
210 }
211
212 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
213 if err != nil {
214 return nil, fmt.Errorf("unable to create request: %w", err)
215 }
216
217 req.Header.Set("Accept", "application/json")
218
219 if payload != nil {
220 req.Header.Set("Content-Type", "application/json")
221 }
222
223 return req, nil
224 }
225
226 func parseError(req *http.Request, resp *http.Response) error {
227 raw, _ := io.ReadAll(resp.Body)
228
229 var response APIResponse[any]
230
231 err := json.Unmarshal(raw, &response)
232 if err != nil {
233 return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
234 }
235
236 return fmt.Errorf("[status code %d] %d: %s", resp.StatusCode, response.Error, codeText(response.Error))
237 }
238