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