diff --git a/pkg/txtutil/txtutil.go b/pkg/txtutil/txtutil.go index ca7718ca7..55045b8c2 100644 --- a/pkg/txtutil/txtutil.go +++ b/pkg/txtutil/txtutil.go @@ -1,5 +1,10 @@ package txtutil +// SplitSingleLongTxt does nothing. +// Deprecated: This is a no-op for backwards compatibility. +func SplitSingleLongTxt(records any) { +} + // ToChunks returns the string as chunks of 255-octet strings (the last string being the remainder). func ToChunks(s string) []string { return splitChunks(s, 255) diff --git a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go index 3b6e2529b..a42fe4b52 100644 --- a/providers/akamaiedgedns/akamaiEdgeDnsProvider.go +++ b/providers/akamaiedgedns/akamaiEdgeDnsProvider.go @@ -17,6 +17,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" ) @@ -105,6 +106,7 @@ func (a *edgeDNSProvider) EnsureZoneExists(domain string) error { // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (a *edgeDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) { + txtutil.SplitSingleLongTxt(existingRecords) keysToUpdate, toReport, err := diff.NewCompat(dc).ChangedGroups(existingRecords) if err != nil { diff --git a/providers/autodns/autoDnsProvider.go b/providers/autodns/autoDnsProvider.go index 91d6f65f8..6da132afe 100644 --- a/providers/autodns/autoDnsProvider.go +++ b/providers/autodns/autoDnsProvider.go @@ -11,6 +11,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" ) @@ -67,6 +68,7 @@ func New(settings map[string]string, _ json.RawMessage) (providers.DNSServicePro // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (api *autoDNSProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) { domain := dc.Name + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records var corrections []*models.Correction diff --git a/providers/axfrddns/axfrddnsProvider.go b/providers/axfrddns/axfrddnsProvider.go index 61496c4ac..f283ee941 100644 --- a/providers/axfrddns/axfrddnsProvider.go +++ b/providers/axfrddns/axfrddnsProvider.go @@ -25,6 +25,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/fatih/color" "github.com/miekg/dns" @@ -409,6 +410,7 @@ func hasNSDeletion(changes diff2.ChangeList) bool { // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (c *axfrddnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) { + txtutil.SplitSingleLongTxt(foundRecords) // Autosplit long TXT records // Ignoring the SOA, others providers don't manage it either. if len(foundRecords) >= 1 && foundRecords[0].Type == "SOA" { diff --git a/providers/azuredns/auditrecords.go b/providers/azuredns/auditrecords.go index 1c3387ec6..13d73026d 100644 --- a/providers/azuredns/auditrecords.go +++ b/providers/azuredns/auditrecords.go @@ -13,7 +13,5 @@ func AuditRecords(records []*models.RecordConfig) []error { a.Add("MX", rejectif.MxNull) // Last verified 2020-12-28 - a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2023-10-27 - return a.Audit(records) } diff --git a/providers/azuredns/azureDnsProvider.go b/providers/azuredns/azureDnsProvider.go index 2d14e8254..105e46e6e 100644 --- a/providers/azuredns/azureDnsProvider.go +++ b/providers/azuredns/azureDnsProvider.go @@ -14,6 +14,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" ) @@ -192,6 +193,8 @@ func (a *azurednsProvider) getExistingRecords(domain string) (models.Records, [] // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (a *azurednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) { + txtutil.SplitSingleLongTxt(existingRecords) // Autosplit long TXT records + var corrections []*models.Correction // Azure is a "ByRecordSet" API. @@ -521,7 +524,8 @@ func (a *azurednsProvider) recordToNativeDiff2(recordKey models.RecordKey, recor if recordSet.Properties.TxtRecords == nil { recordSet.Properties.TxtRecords = []*adns.TxtRecord{} } - if rec.GetTargetTXTJoined() != "" { // Empty TXT record needs to have no value set in it's properties + // Empty TXT record needs to have no value set in it's properties + if !(rec.GetTargetTXTSegmentCount() == 1 && rec.GetTargetTXTSegmented()[0] == "") { var txts []*string for _, txt := range rec.GetTargetTXTSegmented() { txts = append(txts, to.StringPtr(txt)) diff --git a/providers/bind/bindProvider.go b/providers/bind/bindProvider.go index 720a7eb78..91fbff062 100644 --- a/providers/bind/bindProvider.go +++ b/providers/bind/bindProvider.go @@ -25,6 +25,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/pkg/diff2" "github.com/StackExchange/dnscontrol/v4/pkg/prettyzone" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/miekg/dns" ) @@ -194,32 +195,11 @@ func ParseZoneContents(content string, zoneName string, zonefileName string) (mo foundRecords := models.Records{} for rr, ok := zp.Next(); ok; rr, ok = zp.Next() { - header := rr.Header() - rtype := dns.TypeToString[header.Rrtype] - if rtype == "TXT" { - v := rr.(*dns.TXT) - t := strings.Join(v.Txt, "") - //fmt.Fprintf(os.Stdout, "DEBUG: ParseZoneContents inbounds=%s\n", t) - td := strings.ReplaceAll(t, `\"`, `"`) - td = strings.ReplaceAll(td, `\\`, `\`) - //fmt.Fprintf(os.Stdout, "DEBUG: ParseZoneContents decodeds=%s\n", td) - - rec := models.RecordConfig{Type: "TXT"} - rec.SetLabelFromFQDN(strings.TrimSuffix(header.Name, "."), zoneName) - rec.TTL = header.Ttl - rec.Original = rr - err := rec.SetTargetTXT(td) - if err != nil { - return nil, err - } - foundRecords = append(foundRecords, &rec) - } else { - rec, err := models.RRtoRC(rr, zoneName) - if err != nil { - return nil, err - } - foundRecords = append(foundRecords, &rec) + rec, err := models.RRtoRC(rr, zoneName) + if err != nil { + return nil, err } + foundRecords = append(foundRecords, &rec) } if err := zp.Err(); err != nil { @@ -230,6 +210,7 @@ func ParseZoneContents(content string, zoneName string, zonefileName string) (mo // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (c *bindProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) { + txtutil.SplitSingleLongTxt(dc.Records) var corrections []*models.Correction changes := false diff --git a/providers/cloudflare/auditrecords.go b/providers/cloudflare/auditrecords.go index 20f6c595c..71a66a06e 100644 --- a/providers/cloudflare/auditrecords.go +++ b/providers/cloudflare/auditrecords.go @@ -11,11 +11,11 @@ import ( func AuditRecords(records []*models.RecordConfig) []error { a := rejectif.Auditor{} - //a.Add("TXT", rejectif.TxtLongerThan255) // Last verified 2022-06-18 + a.Add("TXT", rejectif.TxtHasMultipleSegments) // Last verified 2022-06-18 - a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2023-11-12 + a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2022-06-18 - a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2023-11-12 + a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2022-06-18 return a.Audit(records) } diff --git a/providers/cloudns/auditrecords.go b/providers/cloudns/auditrecords.go index 1987dd669..20d7b1d1d 100644 --- a/providers/cloudns/auditrecords.go +++ b/providers/cloudns/auditrecords.go @@ -19,7 +19,7 @@ func AuditRecords(records []*models.RecordConfig) []error { a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2021-03-01 - a.Add("TXT", rejectif.TxtLongerThan255) // Last verified 2021-03-01 + a.Add("TXT", rejectif.TxtHasMultipleSegments) // Last verified 2021-03-01 a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2023-03-30 diff --git a/providers/cscglobal/auditrecords.go b/providers/cscglobal/auditrecords.go index 66d8d5edf..1ee9faf8c 100644 --- a/providers/cscglobal/auditrecords.go +++ b/providers/cscglobal/auditrecords.go @@ -17,7 +17,7 @@ func AuditRecords(records []*models.RecordConfig) []error { a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2022-08-08 - a.Add("TXT", rejectif.TxtLongerThan255) // Last verified 2022-06-10 + a.Add("TXT", rejectif.TxtHasMultipleSegments) // Last verified 2022-06-10 a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2022-06-10 diff --git a/providers/desec/desecProvider.go b/providers/desec/desecProvider.go index 4142e463f..d67fa73cb 100644 --- a/providers/desec/desecProvider.go +++ b/providers/desec/desecProvider.go @@ -4,10 +4,12 @@ import ( "bytes" "encoding/json" "fmt" + "sort" "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/miekg/dns/dnsutil" ) @@ -149,6 +151,8 @@ func PrepDesiredRecords(dc *models.DomainConfig, minTTL uint32) { // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (c *desecProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) { + txtutil.SplitSingleLongTxt(dc.Records) + var minTTL uint32 c.mutex.Lock() if ttl, ok := c.domainIndex[dc.Name]; !ok { @@ -233,7 +237,7 @@ func (c *desecProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exist // However the code doesn't seem to have such situation. All tests // pass. That said, if this breaks anything, the easiest fix might // be to just remove the sort. - //sort.Slice(corrections, func(i, j int) bool { return diff.CorrectionLess(corrections, i, j) }) + sort.Slice(corrections, func(i, j int) bool { return diff.CorrectionLess(corrections, i, j) }) return corrections, nil } diff --git a/providers/digitalocean/auditrecords.go b/providers/digitalocean/auditrecords.go index 6331d566d..95382cc92 100644 --- a/providers/digitalocean/auditrecords.go +++ b/providers/digitalocean/auditrecords.go @@ -19,11 +19,9 @@ func AuditRecords(records []*models.RecordConfig) []error { a.Add("TXT", MaxLengthDO) // Last verified 2021-03-01 - a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2023-11-12 - // The web portal rejects blackslashes too - - a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2023-11-12 - // The web portal rejects double quotes + a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2021-03-01 + // Double-quotes not permitted in TXT strings. I have a hunch that + // this is due to a broken parser on the DO side. return a.Audit(records) } @@ -46,7 +44,7 @@ func MaxLengthDO(rc *models.RecordConfig) error { // In other words, they're doing the checking on the API protocol // encoded data instead of on on the resulting TXT record. Sigh. - if len(rc.GetTargetTXTJoined()) > 509 { + if len(rc.GetTargetRFC1035Quoted()) > 509 { return fmt.Errorf("encoded txt too long") } diff --git a/providers/digitalocean/digitaloceanProvider.go b/providers/digitalocean/digitaloceanProvider.go index 24c6298c1..62ec0d7dd 100644 --- a/providers/digitalocean/digitaloceanProvider.go +++ b/providers/digitalocean/digitaloceanProvider.go @@ -10,7 +10,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" - "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/digitalocean/godo" "github.com/miekg/dns/dnsutil" @@ -168,6 +168,8 @@ func (api *digitaloceanProvider) GetZoneRecords(domain string, meta map[string]s // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (api *digitaloceanProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) { + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records + ctx := context.Background() toReport, toCreate, toDelete, toModify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords) @@ -298,7 +300,6 @@ func toRc(domain string, r *godo.DomainRecord) *models.RecordConfig { t.SetLabelFromFQDN(name, domain) switch rtype := r.Type; rtype { case "TXT": - printer.Printf("DEBUG: DIGITAL TXT inbounds=%s q=%q\n", target, target) t.SetTargetTXT(target) default: t.SetTarget(target) @@ -324,7 +325,6 @@ func toReq(dc *models.DomainConfig, rc *models.RecordConfig) *godo.DomainRecordE case "TXT": // TXT records are the one place where DO combines many items into one field. target = rc.GetTargetTXTJoined() - printer.Printf("DEBUG: DIGITAL TXT outbounds=%s q=%q\n", target, target) default: // no action required } diff --git a/providers/dnsimple/auditrecords.go b/providers/dnsimple/auditrecords.go index 3e8df5b63..0354a63eb 100644 --- a/providers/dnsimple/auditrecords.go +++ b/providers/dnsimple/auditrecords.go @@ -13,7 +13,7 @@ func AuditRecords(records []*models.RecordConfig) []error { a.Add("MX", rejectif.MxNull) // Last verified 2023-03 - a.Add("TXT", rejectif.TxtLongerThan255) // Last verified 2023-03 + a.Add("TXT", rejectif.TxtHasMultipleSegments) // Last verified 2023-03 a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2023-03 diff --git a/providers/dnsmadeeasy/dnsMadeEasyProvider.go b/providers/dnsmadeeasy/dnsMadeEasyProvider.go index 2917ad82e..3e6f1eb31 100644 --- a/providers/dnsmadeeasy/dnsMadeEasyProvider.go +++ b/providers/dnsmadeeasy/dnsMadeEasyProvider.go @@ -8,6 +8,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" ) @@ -98,6 +99,8 @@ func New(settings map[string]string, _ json.RawMessage) (providers.DNSServicePro // } func (api *dnsMadeEasyProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) { + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records + domainName := dc.Name domain, err := api.findDomain(domainName) if err != nil { diff --git a/providers/gandiv5/convert.go b/providers/gandiv5/convert.go index e4095bd5f..229ac0c3f 100644 --- a/providers/gandiv5/convert.go +++ b/providers/gandiv5/convert.go @@ -7,7 +7,6 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/printer" - "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/go-gandi/go-gandi/livedns" ) @@ -31,18 +30,7 @@ func nativeToRecords(n livedns.DomainRecord, origin string) (rcs []*models.Recor rc.Type = "ALIAS" err = rc.SetTarget(value) case "TXT": - t := value - //printer.Printf("DEBUG gandi txt inbounds=%s q=%q\n", t, t) - td, err := txtutil.ParseQuoted(t) - if err != nil { - return nil, err - } - //td := t - //printer.Printf("DEBUG gandi txt decodeds=%s q=%q\n", td, td) - err = rc.SetTargetTXT(td) - if err != nil { - return nil, err - } + err = rc.SetTargetTXTfromRFC1035Quoted(value) default: err = rc.PopulateFromString(rtype, value, origin) } @@ -71,27 +59,17 @@ func recordsToNative(rcs []*models.RecordConfig, origin string) []livedns.Domain } key := r.Key() - var val string - if r.Type == "TXT" { - t := r.GetTargetTXTJoined() - //printer.Printf("DEBUG: txt outbounds=%s q=%q\n", t, t) - val = txtutil.EncodeQuoted(t) - //printer.Printf("DEBUG: txt encodeds=%s q=%q\n", val, val) - } else { - val = r.GetTargetCombined() - } - if zr, ok := keys[key]; !ok { // Allocate a new ZoneRecord: zr := livedns.DomainRecord{ RrsetType: r.Type, RrsetTTL: int(r.TTL), RrsetName: label, - RrsetValues: []string{val}, + RrsetValues: []string{r.GetTargetCombined()}, } keys[key] = &zr } else { - zr.RrsetValues = append(zr.RrsetValues, val) + zr.RrsetValues = append(zr.RrsetValues, r.GetTargetCombined()) if r.TTL != uint32(zr.RrsetTTL) { printer.Warnf("All TTLs for a rrset (%v) must be the same. Using smaller of %v and %v.\n", key, r.TTL, zr.RrsetTTL) @@ -99,6 +77,7 @@ func recordsToNative(rcs []*models.RecordConfig, origin string) []livedns.Domain zr.RrsetTTL = int(r.TTL) } } + } } diff --git a/providers/gandiv5/gandi_v5Provider.go b/providers/gandiv5/gandi_v5Provider.go index d4c59d467..d6fb5409f 100644 --- a/providers/gandiv5/gandi_v5Provider.go +++ b/providers/gandiv5/gandi_v5Provider.go @@ -24,6 +24,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/go-gandi/go-gandi" "github.com/go-gandi/go-gandi/config" @@ -175,6 +176,9 @@ func PrepDesiredRecords(dc *models.DomainConfig) { printer.Warnf("Gandi does not support ttls > 30 days. Setting %s from %d to 2592000\n", rec.GetLabelFQDN(), rec.TTL) rec.TTL = 2592000 } + if rec.Type == "TXT" { + rec.SetTarget("\"" + rec.GetTargetField() + "\"") // FIXME(tlim): Should do proper quoting. + } if rec.Type == "NS" && rec.GetLabel() == "@" { if !strings.HasSuffix(rec.GetTargetField(), ".gandi.net.") { printer.Warnf("Gandi does not support changing apex NS records. Ignoring %s\n", rec.GetTargetField()) @@ -194,6 +198,7 @@ func (client *gandiv5Provider) GetZoneRecordsCorrections(dc *models.DomainConfig } PrepDesiredRecords(dc) + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records g := gandi.NewLiveDNSClient(config.Config{ APIKey: client.apikey, diff --git a/providers/gcloud/gcloudProvider.go b/providers/gcloud/gcloudProvider.go index b6b693c98..d0c9e47df 100644 --- a/providers/gcloud/gcloudProvider.go +++ b/providers/gcloud/gcloudProvider.go @@ -259,6 +259,8 @@ type correctionValues struct { // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (g *gcloudProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) { + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records + oldRRs, ok := g.oldRRsMap[dc.Name] if !ok { return nil, fmt.Errorf("oldRRsMap: no zone named %q", dc.Name) @@ -304,22 +306,7 @@ func (g *gcloudProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, exis } for _, r := range dc.Records { if keyForRec(r) == ck { - if ck.Type == "TXT" { - // NB(tlim): These next two lines should not be merged. The - // chunks need to be allocated in a way that the memory is - // not re-used in the next iteration. - //chunks := txtutil.ToChunks(r.GetTargetField()) - //printer.Printf("DEBUG: gcloud txt chunks=%+v\n", chunks) - //newRRs.Rrdatas = append(newRRs.Rrdatas, chunks[0:]...) - t := r.GetTargetField() - //printer.Printf("DEBUG: gcloud outboundv=%v\n", t) - //tc := txtutil.RFC1035ChunkedAndQuoted(t) - tc := txtutil.EncodeQuoted(t) - //printer.Printf("DEBUG: gcloud encodedv=%v\n", tc) - newRRs.Rrdatas = append(newRRs.Rrdatas, tc) - } else { - newRRs.Rrdatas = append(newRRs.Rrdatas, r.GetTargetCombined()) - } + newRRs.Rrdatas = append(newRRs.Rrdatas, r.GetTargetCombined()) newRRs.Ttl = int64(r.TTL) } } @@ -416,7 +403,13 @@ func nativeToRecord(set *gdns.ResourceRecordSet, rec, origin string) (*models.Re r.SetLabelFromFQDN(set.Name, origin) r.TTL = uint32(set.Ttl) rtype := set.Type - err := r.PopulateFromString(rtype, rec, origin) + var err error + switch rtype { + case "TXT": + err = r.SetTargetTXTs(models.ParseQuotedTxt(rec)) + default: + err = r.PopulateFromString(rtype, rec, origin) + } if err != nil { return nil, fmt.Errorf("unparsable record %q received from GCLOUD: %w", rtype, err) } diff --git a/providers/gcore/convert.go b/providers/gcore/convert.go index c234e7d81..b5c1cc6bf 100644 --- a/providers/gcore/convert.go +++ b/providers/gcore/convert.go @@ -86,7 +86,7 @@ func recordsToNative(rcs []*models.RecordConfig, expectedKey models.RecordKey) * } case "TXT": // Avoid double quoting for TXT records rr = dnssdk.ResourceRecord{ - Content: convertTxtSliceToSdkAnySlice([]string{r.GetTargetCombined()}), + Content: convertTxtSliceToSdkAnySlice(r.GetTargetTXTSegmented()), Meta: nil, Enabled: true, } diff --git a/providers/hedns/hednsProvider.go b/providers/hedns/hednsProvider.go index 226754293..0ee6d1485 100644 --- a/providers/hedns/hednsProvider.go +++ b/providers/hedns/hednsProvider.go @@ -191,6 +191,9 @@ func (c *hednsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, recor } } + // Normalize + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records + return c.getDiff2DomainCorrections(dc, zoneID, prunedRecords) } @@ -323,10 +326,7 @@ func (c *hednsProvider) GetZoneRecords(domain string, meta map[string]string) (m rc.Type = "TXT" fallthrough case "TXT": - //err = rc.SetTargetTXTs(models.ParseQuotedTxt(data)) - var t string - t, err = txtutil.ParseQuoted(data) - err = rc.SetTargetTXT(t) + err = rc.SetTargetTXTs(models.ParseQuotedTxt(data)) default: err = rc.PopulateFromString(rc.Type, data, domain) } diff --git a/providers/hetzner/hetznerProvider.go b/providers/hetzner/hetznerProvider.go index fb638f39e..0c373c94c 100644 --- a/providers/hetzner/hetznerProvider.go +++ b/providers/hetzner/hetznerProvider.go @@ -7,6 +7,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" ) @@ -71,6 +72,8 @@ func (api *hetznerProvider) EnsureZoneExists(domain string) error { func (api *hetznerProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) { domain := dc.Name + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records + toReport, create, del, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords) if err != nil { return nil, err diff --git a/providers/hexonet/auditrecords.go b/providers/hexonet/auditrecords.go index 37887afb0..371bd4937 100644 --- a/providers/hexonet/auditrecords.go +++ b/providers/hexonet/auditrecords.go @@ -13,8 +13,6 @@ func AuditRecords(records []*models.RecordConfig) []error { a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2021-10-01 - a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2023-10-27 - a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2020-12-28 return a.Audit(records) diff --git a/providers/hexonet/records.go b/providers/hexonet/records.go index 3ef81b5ac..ac313f714 100644 --- a/providers/hexonet/records.go +++ b/providers/hexonet/records.go @@ -9,6 +9,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" ) // HXRecord covers an individual DNS resource record. @@ -57,6 +58,8 @@ func (n *HXClient) GetZoneRecords(domain string, meta map[string]string) (models // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (n *HXClient) GetZoneRecordsCorrections(dc *models.DomainConfig, actual models.Records) ([]*models.Correction, error) { + txtutil.SplitSingleLongTxt(dc.Records) + toReport, create, del, mod, err := diff.NewCompat(dc).IncrementalDiff(actual) if err != nil { return nil, err @@ -239,7 +242,7 @@ func (n *HXClient) createRecordString(rc *models.RecordConfig, domain string) (s case "CAA": record.Answer = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, record.Answer) case "TXT": - record.Answer = encodeTxt([]string{rc.GetTargetField()}) + record.Answer = encodeTxt(rc.GetTargetTXTSegmented()) case "SRV": if rc.GetTargetField() == "." { return "", fmt.Errorf("SRV records with empty targets are not supported (as of 2020-02-27, the API returns 'Invalid attribute value syntax')") diff --git a/providers/hostingde/types.go b/providers/hostingde/types.go index dee33cc7f..4f789ba2a 100644 --- a/providers/hostingde/types.go +++ b/providers/hostingde/types.go @@ -193,9 +193,9 @@ func recordToNative(rc *models.RecordConfig) *record { case "A", "AAAA", "ALIAS", "CAA", "CNAME", "DNSKEY", "DS", "NS", "NSEC", "NSEC3", "NSEC3PARAM", "PTR", "RRSIG", "SSHFP", "TSLA": // Nothing special. case "TXT": - //txtStrings := make([]string, len(rc.TxtStrings)) - //copy(txtStrings, rc.TxtStrings) - txtStrings := []string{rc.GetTargetField()} + // TODO(tlim): Move this to a function with unit tests. + txtStrings := make([]string, rc.GetTargetTXTSegmentCount()) + copy(txtStrings, rc.GetTargetTXTSegmented()) // Escape quotes for i := range txtStrings { diff --git a/providers/inwx/inwxProvider.go b/providers/inwx/inwxProvider.go index 62ff9242c..a2de4cb03 100644 --- a/providers/inwx/inwxProvider.go +++ b/providers/inwx/inwxProvider.go @@ -10,6 +10,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/nrdcg/goinwx" "github.com/pquerna/otp/totp" @@ -215,9 +216,10 @@ func checkRecords(records models.Records) error { // TODO(tlim) Remove this function. auditrecords.go takes care of this now. for _, r := range records { if r.Type == "TXT" { - target := r.GetTargetField() - if strings.ContainsAny(target, "`") { - return fmt.Errorf("INWX TXT records do not support single-quotes in their target") + for _, target := range r.GetTargetTXTSegmented() { + if strings.ContainsAny(target, "`") { + return fmt.Errorf("INWX TXT records do not support single-quotes in their target") + } } } } @@ -226,6 +228,9 @@ func checkRecords(records models.Records) error { // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (api *inwxAPI) GetZoneRecordsCorrections(dc *models.DomainConfig, foundRecords models.Records) ([]*models.Correction, error) { + + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records + err := checkRecords(dc.Records) if err != nil { return nil, err diff --git a/providers/loopia/auditrecords.go b/providers/loopia/auditrecords.go index 71988b6d3..d9d8cb2d5 100644 --- a/providers/loopia/auditrecords.go +++ b/providers/loopia/auditrecords.go @@ -1,6 +1,8 @@ package loopia import ( + "fmt" + "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/rejectif" ) @@ -23,6 +25,10 @@ func AuditRecords(records []*models.RecordConfig) []error { // TxtHasSegmentLen450orLonger audits TXT records for strings that are >450 octets. func TxtHasSegmentLen450orLonger(rc *models.RecordConfig) error { - // No longer needed. We always generate segments that are 255 octets or fewer. + for _, txt := range rc.GetTargetTXTSegmented() { + if len(txt) > 450 { + return fmt.Errorf("%q txtstring length > 450", rc.GetLabel()) + } + } return nil } diff --git a/providers/loopia/loopiaProvider.go b/providers/loopia/loopiaProvider.go index 25e391e47..96815a9b5 100644 --- a/providers/loopia/loopiaProvider.go +++ b/providers/loopia/loopiaProvider.go @@ -25,6 +25,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/miekg/dns/dnsutil" ) @@ -271,6 +272,7 @@ func (c *APIClient) GetZoneRecordsCorrections(dc *models.DomainConfig, existingR } // Normalize + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records PrepDesiredRecords(dc) var keysToUpdate map[models.RecordKey][]string diff --git a/providers/msdns/auditrecords.go b/providers/msdns/auditrecords.go index d753677c2..f031369f6 100644 --- a/providers/msdns/auditrecords.go +++ b/providers/msdns/auditrecords.go @@ -19,7 +19,7 @@ func AuditRecords(records []*models.RecordConfig) []error { a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2023-02-02 - a.Add("TXT", rejectif.TxtLongerThan255) // Last verified 2023-02-02 + a.Add("TXT", rejectif.TxtHasMultipleSegments) // Last verified 2023-02-02 a.Add("TXT", rejectif.TxtHasSegmentLen256orLonger) // Last verified 2023-02-02 @@ -29,5 +29,7 @@ func AuditRecords(records []*models.RecordConfig) []error { a.Add("TXT", rejectif.TxtIsExactlyLen255) // Last verified 2023-02-02 + a.Add("TXT", rejectif.TxtIsExactlyLen255) // Last verified 2023-02-02 + return a.Audit(records) } diff --git a/providers/msdns/corrections.go b/providers/msdns/corrections.go index 84ad67b15..f58e2ba20 100644 --- a/providers/msdns/corrections.go +++ b/providers/msdns/corrections.go @@ -5,6 +5,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" ) // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. @@ -13,6 +14,7 @@ func (client *msdnsProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, // Normalize models.PostProcessRecords(foundRecords) + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records changes, err := diff2.ByRecord(foundRecords, dc, nil) if err != nil { diff --git a/providers/namedotcom/auditrecords.go b/providers/namedotcom/auditrecords.go index 1de37f47d..2d9a64c84 100644 --- a/providers/namedotcom/auditrecords.go +++ b/providers/namedotcom/auditrecords.go @@ -14,15 +14,13 @@ import ( func AuditRecords(records []*models.RecordConfig) []error { a := rejectif.Auditor{} - a.Add("MX", rejectif.MxNull) // Last verified 2023-10-25 + a.Add("MX", rejectif.MxNull) // Last verified 2020-12-28 - a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2023-10-25 + a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2020-12-28 - a.Add("TXT", MaxLengthNDC) // Last verified 2023-10-25 + a.Add("TXT", MaxLengthNDC) // Last verified 2021-03-01 - a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2023-10-25 - - a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2023-10-25 + a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2021-03-01 return a.Audit(records) } @@ -32,8 +30,7 @@ func AuditRecords(records []*models.RecordConfig) []error { // length limit is undocumented. This seems to work. func MaxLengthNDC(rc *models.RecordConfig) error { txtStrings := rc.GetTargetTXTSegmented() - - if rc.GetTargetTXTJoined() == "" { + if len(txtStrings) == 0 { return nil } diff --git a/providers/ns1/auditrecords.go b/providers/ns1/auditrecords.go index 3d886a767..d30fc8610 100644 --- a/providers/ns1/auditrecords.go +++ b/providers/ns1/auditrecords.go @@ -11,7 +11,7 @@ import ( func AuditRecords(records []*models.RecordConfig) []error { a := rejectif.Auditor{} - a.Add("TXT", rejectif.TxtLongerThan255) + a.Add("TXT", rejectif.TxtHasMultipleSegments) return a.Audit(records) } diff --git a/providers/ns1/ns1Provider.go b/providers/ns1/ns1Provider.go index 8532e1dfd..a92db2860 100644 --- a/providers/ns1/ns1Provider.go +++ b/providers/ns1/ns1Provider.go @@ -9,7 +9,6 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff2" - "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" "gopkg.in/ns1/ns1-go.v2/rest" "gopkg.in/ns1/ns1-go.v2/rest/model/dns" @@ -303,7 +302,7 @@ func buildRecord(recs models.Records, domain string, id string) *dns.Record { if r.Type == "MX" { rec.AddAnswer(&dns.Answer{Rdata: strings.Fields(fmt.Sprintf("%d %v", r.MxPreference, r.GetTargetField()))}) } else if r.Type == "TXT" { - rec.AddAnswer(&dns.Answer{Rdata: txtutil.ToChunks(r.GetTargetField())}) + rec.AddAnswer(&dns.Answer{Rdata: r.GetTargetTXTSegmented()}) } else if r.Type == "CAA" { rec.AddAnswer(&dns.Answer{ Rdata: []string{ diff --git a/providers/oracle/oracleProvider.go b/providers/oracle/oracleProvider.go index aac6204fc..892e6cee8 100644 --- a/providers/oracle/oracleProvider.go +++ b/providers/oracle/oracleProvider.go @@ -9,6 +9,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" "github.com/StackExchange/dnscontrol/v4/providers" "github.com/oracle/oci-go-sdk/v32/common" "github.com/oracle/oci-go-sdk/v32/dns" @@ -203,6 +204,7 @@ func (o *oracleProvider) GetZoneRecords(zone string, meta map[string]string) (mo // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (o *oracleProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) { var err error + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records // Ensure we don't emit changes for attempted modification of built-in apex NSs for _, rec := range dc.Records { diff --git a/providers/powerdns/convert_test.go b/providers/powerdns/convert_test.go index 5612ad299..a88fd2910 100644 --- a/providers/powerdns/convert_test.go +++ b/providers/powerdns/convert_test.go @@ -17,21 +17,19 @@ func TestToRecordConfig(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "test.example.com", recordConfig.NameFQDN) - assert.Equal(t, "simple", recordConfig.GetTargetTXTJoined()) + assert.Equal(t, "\"simple\"", recordConfig.String()) assert.Equal(t, uint32(120), recordConfig.TTL) assert.Equal(t, "TXT", recordConfig.Type) - largeContent := fmt.Sprintf(`"%s" "%s"`, strings.Repeat("A", 300), strings.Repeat("B", 300)) + largeContent := fmt.Sprintf("\"%s\" \"%s\"", strings.Repeat("A", 300), strings.Repeat("B", 300)) largeRecord := zones.Record{ Content: largeContent, } recordConfig, err = toRecordConfig("example.com", largeRecord, 5, "large", "TXT") - //largeJoined := `"` + strings.Repeat("A", 300) + strings.Repeat("B", 300) + `"` - largeJoined := strings.Repeat("A", 300) + strings.Repeat("B", 300) assert.NoError(t, err) assert.Equal(t, "large.example.com", recordConfig.NameFQDN) - assert.Equal(t, largeJoined, recordConfig.GetTargetTXTJoined()) + assert.Equal(t, largeContent, recordConfig.String()) assert.Equal(t, uint32(5), recordConfig.TTL) assert.Equal(t, "TXT", recordConfig.Type) } diff --git a/providers/route53/route53Provider.go b/providers/route53/route53Provider.go index 6c34d0f5c..fd3e7dd1f 100644 --- a/providers/route53/route53Provider.go +++ b/providers/route53/route53Provider.go @@ -278,6 +278,8 @@ func (r *route53Provider) getZoneRecords(zone r53Types.HostedZone) (models.Recor // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (r *route53Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) { + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records + zone, err := r.getZone(dc) if err != nil { return nil, err @@ -341,35 +343,10 @@ func (r *route53Provider) GetZoneRecordsCorrections(dc *models.DomainConfig, exi } for _, r := range inst.New { - - var rr r53Types.ResourceRecord - if instType == "TXT" { - // //printer.Printf("DEBUG: txt originalv=%v\n", r.GetTargetField()) - // //t := txtutil.RFC1035ChunkedAndQuoted(r.GetTargetField()) - // //printer.Printf("DEBUG: txt outbound=%q\n", t) - // ts := r.GetTargetTXTChunked255() - // //t = strings.ReplaceAll(t, `\`, `\\`) - // //t = strings.ReplaceAll(t, `"`, `\"`) - // for i := range ts { - // ts[i] = strings.ReplaceAll(ts[i], `\`, `\\`) - // ts[i] = strings.ReplaceAll(ts[i], `"`, `\"`) - // } - // t := `"` + strings.Join(ts, `" "`) + `"` - // printer.Printf("DEBUG: txt outboundv=%v\n", t) - - t := txtutil.EncodeQuoted(r.GetTargetField()) - //printer.Printf("XXXXXXXXX %v\n", t) - - rr = r53Types.ResourceRecord{ - Value: aws.String(t), - } - } else { - rr = r53Types.ResourceRecord{ - Value: aws.String(r.GetTargetCombined()), - } + rr := r53Types.ResourceRecord{ + Value: aws.String(r.GetTargetCombined()), } rrset.ResourceRecords = append(rrset.ResourceRecords, rr) - i := int64(r.TTL) rrset.TTL = &i } @@ -515,20 +492,7 @@ func nativeToRecords(set r53Types.ResourceRecordSet, origin string) ([]*models.R rc.Original = set switch rtypeString { case "TXT": - //printer.Printf("DEBUG: txt inboundv=%v\n", val) - //printer.Printf("DEBUG: txt decoded=%v\n", models.ParseQuotedTxt(val)[0]) - //err = rc.SetTargetTXTs(models.ParseQuotedTxt(val)) - - //dt, _ := models.ParseQuotedFields(val) - //printer.Printf("DEBUG: txt decodedv=%v\n", dt) - //err = rc.SetTargetTXTs(dt) - - var t string - t, err = txtutil.ParseQuoted(val) - if err == nil { - err = rc.SetTargetTXT(t) - } - + err = rc.SetTargetTXTs(models.ParseQuotedTxt(val)) default: err = rc.PopulateFromString(rtypeString, val, origin) } diff --git a/providers/rwth/auditrecords.go b/providers/rwth/auditrecords.go index a592cfb73..fee9c390c 100644 --- a/providers/rwth/auditrecords.go +++ b/providers/rwth/auditrecords.go @@ -11,7 +11,7 @@ import ( func AuditRecords(records []*models.RecordConfig) []error { a := rejectif.Auditor{} - a.Add("TXT", rejectif.TxtLongerThan255) + a.Add("TXT", rejectif.TxtHasMultipleSegments) a.Add("TXT", rejectif.TxtHasTrailingSpace) diff --git a/providers/rwth/dns.go b/providers/rwth/dns.go index 5ab10d9fc..88b31e6e7 100644 --- a/providers/rwth/dns.go +++ b/providers/rwth/dns.go @@ -5,6 +5,7 @@ import ( "github.com/StackExchange/dnscontrol/v4/models" "github.com/StackExchange/dnscontrol/v4/pkg/diff" + "github.com/StackExchange/dnscontrol/v4/pkg/txtutil" ) // RWTHDefaultNs is the default DNS NS for this provider. @@ -30,6 +31,7 @@ func (api *rwthProvider) GetNameservers(domain string) ([]*models.Nameserver, er // GetZoneRecordsCorrections returns a list of corrections that will turn existing records into dc.Records. func (api *rwthProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, error) { + txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records domain := dc.Name toReport, create, del, modify, err := diff.NewCompat(dc).IncrementalDiff(existingRecords) diff --git a/providers/vultr/auditrecords.go b/providers/vultr/auditrecords.go index 669a3029c..e6aed4759 100644 --- a/providers/vultr/auditrecords.go +++ b/providers/vultr/auditrecords.go @@ -17,7 +17,7 @@ func AuditRecords(records []*models.RecordConfig) []error { // Needs investigation. Could be a dnscontrol issue or // the provider doesn't support double quotes. - a.Add("TXT", rejectif.TxtLongerThan255) + a.Add("TXT", rejectif.TxtHasMultipleSegments) a.Add("CAA", rejectif.CaaTargetContainsWhitespace) // Last verified 2023-01-19