From befb52be86d0b19587a0c5e0968466b995d8b7f4 Mon Sep 17 00:00:00 2001 From: Amelia Aronsohn Date: Fri, 22 Jul 2022 06:36:28 -0700 Subject: [PATCH] DNSIMPLE: Fix TXT Handling, Second Edition (#1624) * Fix typo and add sandbox information * Use SetTargetTXT in GetZoneRecords This fixes the behavior documented in #1622 Also apply cleanup to GetZoneRecords * Remove SetTargetTXT, does not work in all tests * Set The most correct TXT handling * Well, There's your problem * Add an audit and test for unpaired quotes * Add some commentary Co-authored-by: Tom Limoncelli --- docs/_providers/dnsimple.md | 11 ++++++-- integrationTest/integration_test.go | 2 ++ pkg/recordaudit/txt.go | 16 +++++++++++ providers/dnsimple/auditrecords.go | 16 ++++++++++- providers/dnsimple/dnsimpleProvider.go | 39 +++++++++++++------------- 5 files changed, 62 insertions(+), 22 deletions(-) diff --git a/docs/_providers/dnsimple.md b/docs/_providers/dnsimple.md index 4a874baa5..4874a865f 100644 --- a/docs/_providers/dnsimple.md +++ b/docs/_providers/dnsimple.md @@ -7,16 +7,23 @@ jsId: DNSIMPLE # DNSimple Provider ## Configuration -To use this provider, add an entry to `creds.json` with `TYPE` set to `DIGITALOCEAN` +To use this provider, add an entry to `creds.json` with `TYPE` set to `DNSIMPLE` along with a DNSimple account access token. -Example: +You can also set the `baseurl` to use [DNSimple's free sandbox](https://developer.dnsimple.com/sandbox/) for testing. + +Examples: ```json { "dnsimple": { "TYPE": "DNSIMPLE", "token": "your-dnsimple-account-access-token" + }, + "dnsimple_sandbox": { + "TYPE": "DNSIMPLE", + "baseurl": "https://api.sandbox.dnsimple.com", + "token": "your-sandbox-account-access-token" } } ``` diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index f1f272138..3466b46c1 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -845,6 +845,8 @@ func makeTests(t *testing.T) []*TestGroup { clear(), tc("Create TXT with double-quote", txt("foodq", `quo"te`)), clear(), + tc("Create TXT with double-quotes", txt("foodqs", `q"uo"te`)), + clear(), tc("Create TXT with ws at end", txt("foows1", "with space at end ")), //clear(), // TODO(tlim): Re-add this when we fix the RFC1035 escaped-quotes issue. diff --git a/pkg/recordaudit/txt.go b/pkg/recordaudit/txt.go index 7a5a53f39..d38783d6a 100644 --- a/pkg/recordaudit/txt.go +++ b/pkg/recordaudit/txt.go @@ -141,3 +141,19 @@ func TxtNotEmpty(records []*models.RecordConfig) error { } return nil } + +// TxtNoUnpairedDoubleQuotes audits TXT records for strings that contain unpaired doublequotes. +func TxtNoUnpairedDoubleQuotes(records []*models.RecordConfig) error { + for _, rc := range records { + + if rc.HasFormatIdenticalToTXT() { + for _, txt := range rc.TxtStrings { + if strings.Count(txt, `"`)%2 == 1 { + return fmt.Errorf("txtstring contains unpaired doublequotes") + } + } + } + + } + return nil +} diff --git a/providers/dnsimple/auditrecords.go b/providers/dnsimple/auditrecords.go index 411b67ddd..c10fa308b 100644 --- a/providers/dnsimple/auditrecords.go +++ b/providers/dnsimple/auditrecords.go @@ -8,8 +8,22 @@ import ( // AuditRecords returns an error if any records are not // supportable by this provider. func AuditRecords(records []*models.RecordConfig) error { - if err := recordaudit.TxtNoDoubleQuotes(records); err != nil { + //TODO(onlyhavecans) I think we can support multiple strings. + if err := recordaudit.TxtNoMultipleStrings(records); err != nil { return err } + + if err := recordaudit.TxtNoTrailingSpace(records); err != nil { + return err + } // as of 2022-07 + + if err := recordaudit.TxtNotEmpty(records); err != nil { + return err + } // as of 2022-07 + + if err := recordaudit.TxtNoUnpairedDoubleQuotes(records); err != nil { + return err + } // as of 2022-07 + return nil } diff --git a/providers/dnsimple/dnsimpleProvider.go b/providers/dnsimple/dnsimpleProvider.go index 2e5e25f03..22825fdc5 100644 --- a/providers/dnsimple/dnsimpleProvider.go +++ b/providers/dnsimple/dnsimpleProvider.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/StackExchange/dnscontrol/v3/pkg/printer" "sort" "strconv" "strings" @@ -14,7 +13,7 @@ import ( "github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/pkg/diff" - "github.com/StackExchange/dnscontrol/v3/pkg/txtutil" + "github.com/StackExchange/dnscontrol/v3/pkg/printer" "github.com/StackExchange/dnscontrol/v3/providers" ) @@ -77,51 +76,54 @@ func (c *dnsimpleProvider) GetZoneRecords(domain string) (models.Records, error) if r.Type == "SOA" { continue } + if r.Name == "" { r.Name = "@" } + if r.Type == "CNAME" || r.Type == "MX" || r.Type == "ALIAS" || r.Type == "NS" { r.Content += "." } + // DNSimple adds TXT records that mirror the alias records. // They manage them on ALIAS updates, so pretend they don't exist if r.Type == "TXT" && strings.HasPrefix(r.Content, "ALIAS for ") { continue } + rec := &models.RecordConfig{ TTL: uint32(r.TTL), Original: r, } rec.SetLabel(r.Name, domain) + + var err error switch rtype := r.Type; rtype { case "DNSKEY", "CDNSKEY", "CDS": continue case "ALIAS", "URL": rec.Type = r.Type - if err := rec.SetTarget(r.Content); err != nil { - return nil, fmt.Errorf("unparsable record received from dnsimple: %w", err) - } + err = rec.SetTarget(r.Content) case "DS": - if err := rec.SetTargetDSString(r.Content); err != nil { - return nil, fmt.Errorf("unparsable record received from dnsimple: %w", err) - } + err = rec.SetTargetDSString(r.Content) case "MX": - if err := rec.SetTargetMX(uint16(r.Priority), r.Content); err != nil { - return nil, fmt.Errorf("unparsable record received from dnsimple: %w", err) - } + err = rec.SetTargetMX(uint16(r.Priority), r.Content) case "SRV": parts := strings.Fields(r.Content) if len(parts) == 3 { r.Content += "." } - if err := rec.SetTargetSRVPriorityString(uint16(r.Priority), r.Content); err != nil { - return nil, fmt.Errorf("unparsable record received from dnsimple: %w", err) - } + err = rec.SetTargetSRVPriorityString(uint16(r.Priority), r.Content) + case "TXT": + err = rec.SetTargetTXT(r.Content) default: - if err := rec.PopulateFromString(r.Type, r.Content, domain); err != nil { - return nil, fmt.Errorf("unparsable record received from dnsimple: %w", err) - } + err = rec.PopulateFromString(r.Type, r.Content, domain) } + + if err != nil { + return nil, fmt.Errorf("unparsable record received from dnsimple: %w", err) + } + cleanedRecords = append(cleanedRecords, rec) } @@ -152,7 +154,6 @@ func (c *dnsimpleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod // Normalize models.PostProcessRecords(actual) - txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records differ := diff.New(dc) _, create, del, modify, err := differ.IncrementalDiff(actual) @@ -592,7 +593,7 @@ func getTargetRecordContent(rc *models.RecordConfig) string { case "SRV": return fmt.Sprintf("%d %d %s", rc.SrvWeight, rc.SrvPort, rc.GetTargetField()) case "TXT": - return rc.GetTargetRFC1035Quoted() + return rc.GetTargetTXTJoined() default: return rc.GetTargetField() }