Skip to content

Commit

Permalink
Merge pull request #1266 from jacobbednarz/add-support-for-import-and…
Browse files Browse the repository at this point in the history
…-export-bind-configs

dns: add support for importing and exporting DNS records using BIND file configurations
  • Loading branch information
jacobbednarz committed Apr 14, 2023
2 parents 901f9bf + 40c221a commit 494057e
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/1266.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
dns: add support for importing and exporting DNS records using BIND file configurations
```
146 changes: 146 additions & 0 deletions dns.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package cloudflare

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"time"

"golang.org/x/net/idna"
)

// ErrMissingBINDContents is for when the BIND file contents is required but not set.
var ErrMissingBINDContents = errors.New("required BIND config contents missing")

// DNSRecord represents a DNS record in a zone.
type DNSRecord struct {
CreatedOn time.Time `json:"created_on,omitempty"`
Expand Down Expand Up @@ -104,6 +110,73 @@ func toUTS46ASCII(name string) string {
return name
}

// proxiedRecordsRe is the regular expression for determining if a DNS record
// is proxied or not.
var proxiedRecordsRe = regexp.MustCompile(`(?m)^.*\.\s+1\s+IN\s+CNAME.*$`)

// proxiedRecordImportTemplate is the multipart template for importing *only*
// proxied records. See `nonProxiedRecordImportTemplate` for importing records
// that are not proxied.
var proxiedRecordImportTemplate = `--------------------------BOUNDARY
Content-Disposition: form-data; name="file"; filename="bind.txt"
%s
--------------------------BOUNDARY
Content-Disposition: form-data; name="proxied"
true
--------------------------BOUNDARY--`

// nonProxiedRecordImportTemplate is the multipart template for importing DNS
// records that are not proxed. For importing proxied records, use
// `proxiedRecordImportTemplate`.
var nonProxiedRecordImportTemplate = `--------------------------BOUNDARY
Content-Disposition: form-data; name="file"; filename="bind.txt"
%s
--------------------------BOUNDARY--`

// sanitiseBINDFileInput accepts the BIND file as a string and removes parts
// that are not required for importing or would break the import (like SOA
// records).
func sanitiseBINDFileInput(s string) string {
// Remove SOA records.
soaRe := regexp.MustCompile(`(?m)[\r\n]+^.*IN\s+SOA.*$`)
s = soaRe.ReplaceAllString(s, "")

// Remove all comments.
commentRe := regexp.MustCompile(`(?m)[\r\n]+^.*;;.*$`)
s = commentRe.ReplaceAllString(s, "")

// Swap all the tabs to spaces.
r := strings.NewReplacer(
"\t", " ",
"\n\n", "\n",
)
s = r.Replace(s)
s = strings.TrimSpace(s)

return s
}

// extractProxiedRecords accepts a BIND file (as a string) and returns only the
// proxied DNS records.
func extractProxiedRecords(s string) string {
proxiedOnlyRecords := proxiedRecordsRe.FindAllString(s, -1)
return strings.Join(proxiedOnlyRecords, "\n")
}

// removeProxiedRecords accepts a BIND file (as a string) and returns the file
// contents without any proxied records included.
func removeProxiedRecords(s string) string {
return proxiedRecordsRe.ReplaceAllString(s, "")
}

type ExportDNSRecordsParams struct{}
type ImportDNSRecordsParams struct {
BINDContents string
}

type CreateDNSRecordParams struct {
CreatedOn time.Time `json:"created_on,omitempty" url:"created_on,omitempty"`
ModifiedOn time.Time `json:"modified_on,omitempty" url:"modified_on,omitempty"`
Expand Down Expand Up @@ -277,3 +350,76 @@ func (api *API) DeleteDNSRecord(ctx context.Context, rc *ResourceContainer, reco
}
return nil
}

// ExportDNSRecords returns all DNS records for a zone in the BIND format.
//
// API reference: https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-export-dns-records
func (api *API) ExportDNSRecords(ctx context.Context, rc *ResourceContainer, params ExportDNSRecordsParams) (string, error) {
if rc.Level != ZoneRouteLevel {
return "", ErrRequiredZoneLevelResourceContainer
}

if rc.Identifier == "" {
return "", ErrMissingZoneID
}

uri := fmt.Sprintf("/zones/%s/dns_records/export", rc.Identifier)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return "", err
}

return string(res), nil
}

// ImportDNSRecords takes the contents of a BIND configuration file and imports
// all records at once.
//
// The current state of the API doesn't allow the proxying field to be
// automatically set on records where the TTL is 1. Instead you need to
// explicitly tell the endpoint which records are proxied in the form data. To
// achieve a simpler abstraction, we do the legwork in the method of making the
// two separate API calls (one for proxied and one for non-proxied) instead of
// making the end user know about this detail.
//
// API reference: https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-import-dns-records
func (api *API) ImportDNSRecords(ctx context.Context, rc *ResourceContainer, params ImportDNSRecordsParams) error {
if rc.Level != ZoneRouteLevel {
return ErrRequiredZoneLevelResourceContainer
}

if rc.Identifier == "" {
return ErrMissingZoneID
}

if params.BINDContents == "" {
return ErrMissingBINDContents
}

sanitisedBindData := sanitiseBINDFileInput(params.BINDContents)
nonProxiedRecords := removeProxiedRecords(sanitisedBindData)
proxiedOnlyRecords := extractProxiedRecords(sanitisedBindData)

nonProxiedRecordPayload := []byte(fmt.Sprintf(nonProxiedRecordImportTemplate, nonProxiedRecords))
nonProxiedReqBody := bytes.NewReader(nonProxiedRecordPayload)

uri := fmt.Sprintf("/zones/%s/dns_records/import", rc.Identifier)
multipartUploadHeaders := http.Header{
"Content-Type": {"multipart/form-data; boundary=------------------------BOUNDARY"},
}

_, err := api.makeRequestContextWithHeaders(ctx, http.MethodPost, uri, nonProxiedReqBody, multipartUploadHeaders)
if err != nil {
return err
}

proxiedRecordPayload := []byte(fmt.Sprintf(proxiedRecordImportTemplate, proxiedOnlyRecords))
proxiedReqBody := bytes.NewReader(proxiedRecordPayload)

_, err = api.makeRequestContextWithHeaders(ctx, http.MethodPost, uri, proxiedReqBody, multipartUploadHeaders)
if err != nil {
return err
}

return nil
}

0 comments on commit 494057e

Please sign in to comment.