diff --git a/docs/_includes/matrix.html b/docs/_includes/matrix.html
index 64aa5e128..5fa4d55b1 100644
--- a/docs/_includes/matrix.html
+++ b/docs/_includes/matrix.html
@@ -74,8 +74,8 @@
|
-
-
+ |
+
|
@@ -197,8 +197,8 @@
|
|
-
-
+ |
+
|
@@ -603,7 +603,9 @@
|
|
- |
+
+
+ |
|
@@ -931,7 +933,9 @@
|
- |
+
+
+ |
|
@@ -1830,7 +1834,9 @@
|
- |
+
+
+ |
|
diff --git a/models/quotes.go b/models/quotes.go
index 9528dce2b..49ac054c8 100644
--- a/models/quotes.go
+++ b/models/quotes.go
@@ -20,6 +20,7 @@ func IsQuoted(s string) bool {
}
// StripQuotes returns the string with the starting and ending quotes removed.
+// If it is not quoted, the original string is returned.
func StripQuotes(s string) string {
if IsQuoted(s) {
return s[1 : len(s)-1]
@@ -36,12 +37,20 @@ func ParseQuotedTxt(s string) []string {
if !IsQuoted(s) {
return []string{s}
}
+
+ // TODO(tlim): Consider using r, err := ParseQuotedFields(s)
return strings.Split(StripQuotes(s), `" "`)
}
// ParseQuotedFields is like strings.Fields except individual fields
// might be quoted using `"`.
func ParseQuotedFields(s string) ([]string, error) {
+ // Parse according to RFC1035 zonefile specifications.
+ // "foo" -> one string: `foo``
+ // "foo" "bar" -> two strings: `foo` and `bar`
+ // Quotes are escaped with \"
+
+ // Implementation note:
// Fields are space-separated but a field might be quoted. This is,
// essentially, a CSV where spaces are the field separator (not
// commas). Therefore, we use the CSV parser. See https://stackoverflow.com/a/47489846/71978
diff --git a/models/t_caa.go b/models/t_caa.go
index b389e8976..9086da534 100644
--- a/models/t_caa.go
+++ b/models/t_caa.go
@@ -43,5 +43,5 @@ func (rc *RecordConfig) SetTargetCAAString(s string) error {
if len(part) != 3 {
return fmt.Errorf("CAA value does not contain 3 fields: (%#v)", s)
}
- return rc.SetTargetCAAStrings(part[0], part[1], StripQuotes(part[2]))
+ return rc.SetTargetCAAStrings(part[0], part[1], part[2])
}
diff --git a/models/t_txt.go b/models/t_txt.go
index ec66a2bae..c70bd53cd 100644
--- a/models/t_txt.go
+++ b/models/t_txt.go
@@ -1,5 +1,7 @@
package models
+import "strings"
+
/*
Sadly many providers handle TXT records in strange and non-compliant ways.
@@ -71,11 +73,30 @@ func (rc *RecordConfig) SetTargetTXTs(s []string) error {
return nil
}
+// GetTargetTXTJoined returns the TXT target as one string. If it was stored as multiple strings, concatenate them.
+func (rc *RecordConfig) GetTargetTXTJoined() string {
+ return strings.Join(rc.TxtStrings, "")
+}
+
// SetTargetTXTString is like SetTargetTXT but accepts one big string,
// which must be parsed into one or more strings based on how it is quoted.
// Ex: foo << 1 string
// foo bar << 1 string
+// "foo bar" << 1 string
// "foo" "bar" << 2 strings
+// FIXME(tlim): This function is badly named. It obscures the fact
+// that the string is parsed for quotes and should only be used for returns TXTMulti.
+// Deprecated: Use SetTargetTXTfromRFC1035Quoted instead.
func (rc *RecordConfig) SetTargetTXTString(s string) error {
return rc.SetTargetTXTs(ParseQuotedTxt(s))
}
+
+func (rc *RecordConfig) SetTargetTXTfromRFC1035Quoted(s string) error {
+ many, err := ParseQuotedFields(s)
+ if err != nil {
+ return err
+ }
+ return rc.SetTargetTXTs(many)
+}
+
+// There is no GetTargetTXTfromRFC1025Quoted(). Use GetTargetRFC1035Quoted()
diff --git a/providers/cloudflare/auditrecords.go b/providers/cloudflare/auditrecords.go
index dd1356fbd..805e818fb 100644
--- a/providers/cloudflare/auditrecords.go
+++ b/providers/cloudflare/auditrecords.go
@@ -2,10 +2,24 @@ package cloudflare
import (
"github.com/StackExchange/dnscontrol/v3/models"
+ "github.com/StackExchange/dnscontrol/v3/pkg/recordaudit"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
+
+ if err := recordaudit.TxtNoMultipleStrings(records); err != nil {
+ return err
+ } // Still needed as of 2022-06-18
+
+ if err := recordaudit.TxtNoTrailingSpace(records); err != nil {
+ return err
+ } // Still needed as of 2022-06-18
+
+ if err := recordaudit.TxtNotEmpty(records); err != nil {
+ return err
+ } // Still needed as of 2022-06-18
+
return nil
}
diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go
index 8664a35d6..ad9ba7db8 100644
--- a/providers/cloudflare/cloudflareProvider.go
+++ b/providers/cloudflare/cloudflareProvider.go
@@ -214,6 +214,14 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
// Normalize
models.PostProcessRecords(records)
+ //txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
+ // Don't split.
+ // Cloudflare's API only supports one TXT string of any non-zero length. No
+ // multiple strings (TXTMulti).
+ // When serving the DNS record, it splits strings >255 octets into
+ // individual segments of 255 each. However that is hidden from the API.
+ // Therefore, whether the string is 1 octet or thousands, just store it as
+ // one string in the first element of .TxtStrings.
differ := diff.New(dc, getProxyMetadata)
_, create, del, mod, err := differ.IncrementalDiff(records)
@@ -636,6 +644,7 @@ func stringDefault(value interface{}, def string) string {
}
func (c *cloudflareProvider) nativeToRecord(domain string, cr cloudflare.DNSRecord) (*models.RecordConfig, error) {
+
// normalize cname,mx,ns records with dots to be consistent with our config format.
if cr.Type == "CNAME" || cr.Type == "MX" || cr.Type == "NS" || cr.Type == "PTR" {
if cr.Content != "." {
@@ -670,7 +679,10 @@ func (c *cloudflareProvider) nativeToRecord(domain string, cr cloudflare.DNSReco
target); err != nil {
return nil, fmt.Errorf("unparsable SRV record received from cloudflare: %w", err)
}
- default: // "A", "AAAA", "ANAME", "CAA", "CNAME", "NS", "PTR", "TXT"
+ case "TXT":
+ err := rc.SetTargetTXT(cr.Content)
+ return rc, err
+ default:
if err := rc.PopulateFromString(rType, cr.Content, domain); err != nil {
return nil, fmt.Errorf("unparsable record received from cloudflare: %w", err)
}
diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go
index 4314a980c..7f8024a63 100644
--- a/providers/cloudflare/rest.go
+++ b/providers/cloudflare/rest.go
@@ -47,7 +47,7 @@ func (c *cloudflareProvider) getRecordsForDomain(id string, domain string) ([]*m
// create a correction to delete a record
func (c *cloudflareProvider) deleteRec(rec cloudflare.DNSRecord, domainID string) *models.Correction {
return &models.Correction{
- Msg: fmt.Sprintf("DELETE record: %s %s %d %s (id=%s)", rec.Name, rec.Type, rec.TTL, rec.Content, rec.ID),
+ Msg: fmt.Sprintf("DELETE record: %s %s %d %q (id=%s)", rec.Name, rec.Type, rec.TTL, rec.Content, rec.ID),
F: func() error {
err := c.cfClient.DeleteDNSRecord(context.Background(), domainID, rec.ID)
return err
@@ -119,7 +119,7 @@ func (c *cloudflareProvider) createRec(rec *models.RecordConfig, domainID string
prio = fmt.Sprintf(" %d ", rec.MxPreference)
}
if rec.Type == "TXT" {
- content = rec.GetTargetRFC1035Quoted()
+ content = rec.GetTargetTXTJoined()
}
if rec.Type == "DS" {
content = fmt.Sprintf("%d %d %d %s", rec.DsKeyTag, rec.DsAlgorithm, rec.DsDigestType, rec.DsDigest)
@@ -183,7 +183,7 @@ func (c *cloudflareProvider) modifyRecord(domainID, recID string, proxied bool,
TTL: int(rec.TTL),
}
if rec.Type == "TXT" {
- r.Content = rec.GetTargetRFC1035Quoted()
+ r.Content = rec.GetTargetTXTJoined()
}
if rec.Type == "SRV" {
r.Data = cfSrvData(rec)