client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "encoding/xml"
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 const (
17 apiBaseURL = "https://api.nic.ru/dns-master"
18 tokenURL = "https://api.nic.ru/oauth/token"
19 )
20
21 const successStatus = "success"
22
23 // Trimmer trim all XML fields.
24 type Trimmer struct {
25 decoder *xml.Decoder
26 }
27
28 func (tr Trimmer) Token() (xml.Token, error) {
29 t, err := tr.decoder.Token()
30 if cd, ok := t.(xml.CharData); ok {
31 t = xml.CharData(bytes.TrimSpace(cd))
32 }
33
34 return t, err
35 }
36
37 type Client struct {
38 baseURL *url.URL
39 httpClient *http.Client
40 }
41
42 func NewClient(httpClient *http.Client) (*Client, error) {
43 if httpClient == nil {
44 httpClient = &http.Client{Timeout: 5 * time.Second}
45 }
46
47 baseURL, _ := url.Parse(apiBaseURL)
48
49 return &Client{
50 baseURL: baseURL,
51 httpClient: httpClient,
52 }, nil
53 }
54
55 func (c *Client) GetServices(ctx context.Context) ([]Service, error) {
56 endpoint := c.baseURL.JoinPath("services")
57
58 req, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil)
59 if err != nil {
60 return nil, err
61 }
62
63 apiResponse, err := c.do(req)
64 if err != nil {
65 return nil, err
66 }
67
68 if apiResponse.Data == nil {
69 return nil, nil
70 }
71
72 return apiResponse.Data.Service, nil
73 }
74
75 func (c *Client) ListZones(ctx context.Context) ([]Zone, error) {
76 endpoint := c.baseURL.JoinPath("zones")
77
78 req, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil)
79 if err != nil {
80 return nil, err
81 }
82
83 apiResponse, err := c.do(req)
84 if err != nil {
85 return nil, err
86 }
87
88 if apiResponse.Data == nil {
89 return nil, nil
90 }
91
92 return apiResponse.Data.Zone, nil
93 }
94
95 func (c *Client) GetZonesByService(ctx context.Context, serviceName string) ([]Zone, error) {
96 endpoint := c.baseURL.JoinPath("services", serviceName, "zones")
97
98 req, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil)
99 if err != nil {
100 return nil, err
101 }
102
103 apiResponse, err := c.do(req)
104 if err != nil {
105 return nil, err
106 }
107
108 if apiResponse.Data == nil {
109 return nil, nil
110 }
111
112 return apiResponse.Data.Zone, nil
113 }
114
115 func (c *Client) GetRecords(ctx context.Context, serviceName, zoneName string) ([]RR, error) {
116 endpoint := c.baseURL.JoinPath("services", serviceName, "zones", zoneName, "records")
117
118 req, err := newXMLRequest(ctx, http.MethodGet, endpoint, nil)
119 if err != nil {
120 return nil, err
121 }
122
123 apiResponse, err := c.do(req)
124 if err != nil {
125 return nil, err
126 }
127
128 if apiResponse.Data == nil {
129 return nil, nil
130 }
131
132 var records []RR
133 for _, zone := range apiResponse.Data.Zone {
134 records = append(records, zone.RR...)
135 }
136
137 return records, nil
138 }
139
140 func (c *Client) DeleteRecord(ctx context.Context, serviceName, zoneName, id string) error {
141 endpoint := c.baseURL.JoinPath("services", serviceName, "zones", zoneName, "records", id)
142
143 req, err := newXMLRequest(ctx, http.MethodDelete, endpoint, nil)
144 if err != nil {
145 return err
146 }
147
148 _, err = c.do(req)
149 if err != nil {
150 return err
151 }
152
153 return nil
154 }
155
156 func (c *Client) CommitZone(ctx context.Context, serviceName, zoneName string) error {
157 endpoint := c.baseURL.JoinPath("services", serviceName, "zones", zoneName, "commit")
158
159 req, err := newXMLRequest(ctx, http.MethodPost, endpoint, nil)
160 if err != nil {
161 return err
162 }
163
164 _, err = c.do(req)
165 if err != nil {
166 return err
167 }
168
169 return nil
170 }
171
172 func (c *Client) AddRecords(ctx context.Context, serviceName, zoneName string, rrs []RR) ([]Zone, error) {
173 endpoint := c.baseURL.JoinPath("services", serviceName, "zones", zoneName, "records")
174
175 payload := &Request{RRList: &RRList{RR: rrs}}
176
177 req, err := newXMLRequest(ctx, http.MethodPut, endpoint, payload)
178 if err != nil {
179 return nil, err
180 }
181
182 apiResponse, err := c.do(req)
183 if err != nil {
184 return nil, err
185 }
186
187 if apiResponse.Data == nil {
188 return nil, nil
189 }
190
191 return apiResponse.Data.Zone, nil
192 }
193
194 func (c *Client) do(req *http.Request) (*Response, error) {
195 resp, err := c.httpClient.Do(req)
196 if err != nil {
197 return nil, errutils.NewHTTPDoError(req, err)
198 }
199
200 defer func() { _ = resp.Body.Close() }()
201
202 apiResponse := &Response{}
203
204 raw, err := io.ReadAll(resp.Body)
205 if err != nil {
206 return nil, errutils.NewReadResponseError(req, resp.StatusCode, err)
207 }
208
209 decoder := xml.NewTokenDecoder(Trimmer{decoder: xml.NewDecoder(bytes.NewReader(raw))})
210
211 err = decoder.Decode(apiResponse)
212 if err != nil {
213 return nil, fmt.Errorf("[status code=%d] decode XML response: %s", resp.StatusCode, string(raw))
214 }
215
216 if apiResponse.Status != successStatus {
217 return nil, fmt.Errorf("[status code=%d] %s: %w", resp.StatusCode, apiResponse.Status, apiResponse.Errors.Error)
218 }
219
220 return apiResponse, nil
221 }
222
223 func newXMLRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
224 body := new(bytes.Buffer)
225
226 if payload != nil {
227 body.WriteString(xml.Header)
228
229 encoder := xml.NewEncoder(body)
230 encoder.Indent("", " ")
231
232 err := encoder.Encode(payload)
233 if err != nil {
234 return nil, err
235 }
236 }
237
238 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
239 if err != nil {
240 return nil, fmt.Errorf("unable to create request: %w", err)
241 }
242
243 req.Header.Set("Accept", "text/xml")
244
245 if payload != nil {
246 req.Header.Set("Content-Type", "text/xml")
247 }
248
249 return req, nil
250 }
251