records.go raw
1 package desec
2
3 import (
4 "context"
5 "fmt"
6 "net/http"
7 "net/url"
8 "time"
9 )
10
11 // ApexZone apex zone name.
12 // https://desec.readthedocs.io/en/latest/dns/rrsets.html#accessing-the-zone-apex
13 const ApexZone = "@"
14
15 // IgnoreFilter is a specific value used to ignore a filter field.
16 const IgnoreFilter = "#IGNORE#"
17
18 // RRSet DNS Record Set.
19 type RRSet struct {
20 Name string `json:"name,omitempty"`
21 Domain string `json:"domain,omitempty"`
22 SubName string `json:"subname,omitempty"`
23 Type string `json:"type,omitempty"`
24 Records []string `json:"records"`
25 TTL int `json:"ttl,omitempty"`
26 Created *time.Time `json:"created,omitempty"`
27 Touched *time.Time `json:"touched,omitempty"`
28 }
29
30 // RRSetFilter a RRSets filter.
31 type RRSetFilter struct {
32 Type string
33 SubName string
34 }
35
36 // FilterRRSetOnlyOnType creates an RRSetFilter that ignore SubName.
37 func FilterRRSetOnlyOnType(t string) RRSetFilter {
38 return RRSetFilter{
39 Type: t,
40 SubName: IgnoreFilter,
41 }
42 }
43
44 // FilterRRSetOnlyOnSubName creates an RRSetFilter that ignore Type.
45 func FilterRRSetOnlyOnSubName(n string) RRSetFilter {
46 return RRSetFilter{
47 Type: IgnoreFilter,
48 SubName: n,
49 }
50 }
51
52 // RecordsService handles communication with the records related methods of the deSEC API.
53 //
54 // https://desec.readthedocs.io/en/latest/dns/rrsets.html
55 type RecordsService struct {
56 client *Client
57 }
58
59 /*
60 Domains
61 */
62
63 // GetAll retrieving all RRSets in a zone.
64 // https://desec.readthedocs.io/en/latest/dns/rrsets.html#retrieving-all-rrsets-in-a-zone
65 func (s *RecordsService) GetAll(ctx context.Context, domainName string, filter *RRSetFilter) ([]RRSet, error) {
66 rrSets, _, err := s.GetAllPaginated(ctx, domainName, filter, "")
67 if err != nil {
68 return nil, err
69 }
70
71 return rrSets, nil
72 }
73
74 // GetAllPaginated retrieving all RRSets in a zone.
75 // https://desec.readthedocs.io/en/latest/dns/rrsets.html#retrieving-all-rrsets-in-a-zone
76 func (s *RecordsService) GetAllPaginated(ctx context.Context, domainName string, filter *RRSetFilter, cursor string) ([]RRSet, *Cursors, error) {
77 queryValues := url.Values{}
78
79 if filter != nil {
80 if filter.Type != IgnoreFilter {
81 queryValues.Set("type", filter.Type)
82 }
83
84 if filter.SubName != IgnoreFilter {
85 queryValues.Set("subname", filter.SubName)
86 }
87 }
88
89 queryValues.Set("cursor", cursor)
90
91 rrSets, cursors, err := s.getAll(ctx, domainName, queryValues)
92 if err != nil {
93 return nil, nil, err
94 }
95
96 return rrSets, cursors, nil
97 }
98
99 func (s *RecordsService) getAll(ctx context.Context, domainName string, query url.Values) ([]RRSet, *Cursors, error) {
100 endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets")
101 if err != nil {
102 return nil, nil, fmt.Errorf("failed to create endpoint: %w", err)
103 }
104
105 req, err := s.client.newRequest(ctx, http.MethodGet, endpoint, nil)
106 if err != nil {
107 return nil, nil, err
108 }
109
110 if len(query) > 0 {
111 req.URL.RawQuery = query.Encode()
112 }
113
114 resp, err := s.client.httpClient.Do(req)
115 if err != nil {
116 return nil, nil, fmt.Errorf("failed to call API: %w", err)
117 }
118
119 defer func() { _ = resp.Body.Close() }()
120
121 if resp.StatusCode != http.StatusOK {
122 return nil, nil, handleError(resp)
123 }
124
125 cursors, err := parseCursor(resp.Header)
126 if err != nil {
127 return nil, nil, err
128 }
129
130 var rrSets []RRSet
131
132 err = handleResponse(resp, &rrSets)
133 if err != nil {
134 return nil, nil, err
135 }
136
137 return rrSets, cursors, nil
138 }
139
140 // Create creates a new RRSet.
141 // https://desec.readthedocs.io/en/latest/dns/rrsets.html#creating-a-tlsa-rrset
142 func (s *RecordsService) Create(ctx context.Context, rrSet RRSet) (*RRSet, error) {
143 endpoint, err := s.client.createEndpoint("domains", rrSet.Domain, "rrsets")
144 if err != nil {
145 return nil, fmt.Errorf("failed to create endpoint: %w", err)
146 }
147
148 req, err := s.client.newRequest(ctx, http.MethodPost, endpoint, rrSet)
149 if err != nil {
150 return nil, err
151 }
152
153 resp, err := s.client.httpClient.Do(req)
154 if err != nil {
155 return nil, fmt.Errorf("failed to call API: %w", err)
156 }
157
158 defer func() { _ = resp.Body.Close() }()
159
160 if resp.StatusCode != http.StatusCreated {
161 return nil, handleError(resp)
162 }
163
164 var newRRSet RRSet
165
166 err = handleResponse(resp, &newRRSet)
167 if err != nil {
168 return nil, err
169 }
170
171 return &newRRSet, nil
172 }
173
174 /*
175 Domains + subname + type
176 */
177
178 // Get gets a RRSet.
179 // https://desec.readthedocs.io/en/latest/dns/rrsets.html#retrieving-a-specific-rrset
180 func (s *RecordsService) Get(ctx context.Context, domainName, subName, recordType string) (*RRSet, error) {
181 if subName == "" {
182 subName = ApexZone
183 }
184
185 endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets", subName, recordType)
186 if err != nil {
187 return nil, fmt.Errorf("failed to create endpoint: %w", err)
188 }
189
190 req, err := s.client.newRequest(ctx, http.MethodGet, endpoint, nil)
191 if err != nil {
192 return nil, err
193 }
194
195 resp, err := s.client.httpClient.Do(req)
196 if err != nil {
197 return nil, fmt.Errorf("failed to call API: %w", err)
198 }
199
200 defer func() { _ = resp.Body.Close() }()
201
202 if resp.StatusCode != http.StatusOK {
203 return nil, handleError(resp)
204 }
205
206 var rrSet RRSet
207
208 err = handleResponse(resp, &rrSet)
209 if err != nil {
210 return nil, err
211 }
212
213 return &rrSet, nil
214 }
215
216 // Update updates RRSet (PATCH).
217 // https://desec.readthedocs.io/en/latest/dns/rrsets.html#modifying-an-rrset
218 func (s *RecordsService) Update(ctx context.Context, domainName, subName, recordType string, rrSet RRSet) (*RRSet, error) {
219 if subName == "" {
220 subName = ApexZone
221 }
222
223 endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets", subName, recordType)
224 if err != nil {
225 return nil, fmt.Errorf("failed to create endpoint: %w", err)
226 }
227
228 req, err := s.client.newRequest(ctx, http.MethodPatch, endpoint, rrSet)
229 if err != nil {
230 return nil, err
231 }
232
233 resp, err := s.client.httpClient.Do(req)
234 if err != nil {
235 return nil, fmt.Errorf("failed to call API: %w", err)
236 }
237
238 defer func() { _ = resp.Body.Close() }()
239
240 // when a RRSet is deleted (empty records)
241 if resp.StatusCode == http.StatusNoContent {
242 return nil, nil
243 }
244
245 if resp.StatusCode != http.StatusOK {
246 return nil, handleError(resp)
247 }
248
249 var updatedRRSet RRSet
250
251 err = handleResponse(resp, &updatedRRSet)
252 if err != nil {
253 return nil, err
254 }
255
256 return &updatedRRSet, nil
257 }
258
259 // Replace replaces a RRSet (PUT).
260 // https://desec.readthedocs.io/en/latest/dns/rrsets.html#modifying-an-rrset
261 func (s *RecordsService) Replace(ctx context.Context, domainName, subName, recordType string, rrSet RRSet) (*RRSet, error) {
262 if subName == "" {
263 subName = ApexZone
264 }
265
266 endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets", subName, recordType)
267 if err != nil {
268 return nil, fmt.Errorf("failed to create endpoint: %w", err)
269 }
270
271 req, err := s.client.newRequest(ctx, http.MethodPut, endpoint, rrSet)
272 if err != nil {
273 return nil, err
274 }
275
276 resp, err := s.client.httpClient.Do(req)
277 if err != nil {
278 return nil, fmt.Errorf("failed to call API: %w", err)
279 }
280
281 defer func() { _ = resp.Body.Close() }()
282
283 // when a RRSet is deleted (empty records)
284 if resp.StatusCode == http.StatusNoContent {
285 return nil, nil
286 }
287
288 if resp.StatusCode != http.StatusOK {
289 return nil, handleError(resp)
290 }
291
292 var updatedRRSet RRSet
293
294 err = handleResponse(resp, &updatedRRSet)
295 if err != nil {
296 return nil, err
297 }
298
299 return &updatedRRSet, nil
300 }
301
302 // Delete deletes a RRSet.
303 // https://desec.readthedocs.io/en/latest/dns/rrsets.html#deleting-an-rrset
304 func (s *RecordsService) Delete(ctx context.Context, domainName, subName, recordType string) error {
305 if subName == "" {
306 subName = ApexZone
307 }
308
309 endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets", subName, recordType)
310 if err != nil {
311 return fmt.Errorf("failed to create endpoint: %w", err)
312 }
313
314 req, err := s.client.newRequest(ctx, http.MethodDelete, endpoint, nil)
315 if err != nil {
316 return err
317 }
318
319 resp, err := s.client.httpClient.Do(req)
320 if err != nil {
321 return fmt.Errorf("failed to call API: %w", err)
322 }
323
324 defer func() { _ = resp.Body.Close() }()
325
326 if resp.StatusCode != http.StatusNoContent {
327 return handleError(resp)
328 }
329
330 return nil
331 }
332
333 /*
334 Bulk operations
335 */
336
337 // UpdateMode the mode used to bulk update operations.
338 type UpdateMode string
339
340 const (
341 // FullResource the full resource must be specified.
342 FullResource UpdateMode = http.MethodPut
343 // OnlyFields only fields you would like to modify need to be provided.
344 OnlyFields UpdateMode = http.MethodPatch
345 )
346
347 // BulkCreate creates new RRSets in bulk.
348 // https://desec.readthedocs.io/en/latest/dns/rrsets.html#bulk-creation-of-rrsets
349 func (s *RecordsService) BulkCreate(ctx context.Context, domainName string, rrSets []RRSet) ([]RRSet, error) {
350 endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets")
351 if err != nil {
352 return nil, fmt.Errorf("failed to create endpoint: %w", err)
353 }
354
355 req, err := s.client.newRequest(ctx, http.MethodPost, endpoint, rrSets)
356 if err != nil {
357 return nil, err
358 }
359
360 resp, err := s.client.httpClient.Do(req)
361 if err != nil {
362 return nil, fmt.Errorf("failed to call API: %w", err)
363 }
364
365 defer func() { _ = resp.Body.Close() }()
366
367 if resp.StatusCode != http.StatusCreated {
368 return nil, handleError(resp)
369 }
370
371 var newRRSets []RRSet
372
373 err = handleResponse(resp, &newRRSets)
374 if err != nil {
375 return nil, err
376 }
377
378 return newRRSets, nil
379 }
380
381 // BulkUpdate updates RRSets in bulk.
382 // https://desec.readthedocs.io/en/latest/dns/rrsets.html#bulk-modification-of-rrsets
383 func (s *RecordsService) BulkUpdate(ctx context.Context, mode UpdateMode, domainName string, rrSets []RRSet) ([]RRSet, error) {
384 endpoint, err := s.client.createEndpoint("domains", domainName, "rrsets")
385 if err != nil {
386 return nil, fmt.Errorf("failed to create endpoint: %w", err)
387 }
388
389 req, err := s.client.newRequest(ctx, string(mode), endpoint, rrSets)
390 if err != nil {
391 return nil, err
392 }
393
394 resp, err := s.client.httpClient.Do(req)
395 if err != nil {
396 return nil, fmt.Errorf("failed to call API: %w", err)
397 }
398
399 defer func() { _ = resp.Body.Close() }()
400
401 if resp.StatusCode != http.StatusOK {
402 return nil, handleError(resp)
403 }
404
405 var results []RRSet
406
407 err = handleResponse(resp, &results)
408 if err != nil {
409 return nil, err
410 }
411
412 return results, nil
413 }
414
415 // BulkDelete deletes RRSets in bulk (uses FullResourceUpdateMode).
416 // https://desec.readthedocs.io/en/latest/dns/rrsets.html#bulk-deletion-of-rrsets
417 func (s *RecordsService) BulkDelete(ctx context.Context, domainName string, rrSets []RRSet) error {
418 deleteRRSets := make([]RRSet, len(rrSets))
419
420 for i, rrSet := range rrSets {
421 rrSet.Records = []string{}
422 deleteRRSets[i] = rrSet
423 }
424
425 _, err := s.BulkUpdate(ctx, FullResource, domainName, deleteRRSets)
426 if err != nil {
427 return err
428 }
429
430 return nil
431 }
432