diff --git a/documentation/providers/dnsimple.md b/documentation/providers/dnsimple.md index 6582d0139..a096f9551 100644 --- a/documentation/providers/dnsimple.md +++ b/documentation/providers/dnsimple.md @@ -24,9 +24,11 @@ Examples: {% endcode %} ## Metadata + This provider does not recognize any special metadata fields unique to DNSimple. ## Usage + An example configuration: {% code title="dnsconfig.js" %} @@ -41,8 +43,20 @@ D("example.com", REG_DNSIMPLE, DnsProvider(DSP_DNSIMPLE), {% endcode %} ## Activation + DNSControl depends on a DNSimple account access token. ## Caveats -None at this time +### TXT record length + +The DNSimple API supports TXT records of up to 1000 "characters" (assumed to +be octets, per DNS norms, not Unicode characters in an encoding). + +See https://support.dnsimple.com/articles/txt-record/ + +## Development + +### Debugging + +Set `DNSIMPLE_DEBUG_HTTP` environment variable to `1` to dump all API calls made by this provider. diff --git a/go.mod b/go.mod index d20eeb898..2ece4b113 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/cloudflare/cloudflare-go v0.84.0 github.com/digitalocean/godo v1.107.0 github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c - github.com/dnsimple/dnsimple-go v1.2.0 + github.com/dnsimple/dnsimple-go v1.5.1 github.com/exoscale/egoscale v0.90.2 github.com/go-acme/lego v2.7.2+incompatible github.com/go-gandi/go-gandi v0.7.0 @@ -135,6 +135,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect github.com/stretchr/objx v0.5.0 // indirect diff --git a/go.sum b/go.sum index cda8b0476..23502d2c5 100644 --- a/go.sum +++ b/go.sum @@ -109,8 +109,8 @@ github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c h1:+Zo5Ca9 github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c/go.mod h1:HJGU9ULdREjOcVGZVPB5s6zYmHi1RxzT71l2wQyLmnE= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/dnsimple/dnsimple-go v1.2.0 h1:ddTGyLVKly5HKb5L65AkLqFqwZlWo3WnR0BlFZlIddM= -github.com/dnsimple/dnsimple-go v1.2.0/go.mod h1:z/cs26v/eiRvUyXsHQBLd8lWF8+cD6GbmkPH84plM4U= +github.com/dnsimple/dnsimple-go v1.5.1 h1:zr7OJgQBfS8kJJGMRpcXC6DEeKErR71aNogCMnWGawU= +github.com/dnsimple/dnsimple-go v1.5.1/go.mod h1:QWFwNXg2hV2PrGQE9b69Ig6UwHyIOxQRrECmVvaugss= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -389,6 +389,8 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= diff --git a/providers/dnsimple/auditrecords.go b/providers/dnsimple/auditrecords.go index ab4e4038f..1812c0ae5 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.TxtLongerThan(255)) // Last verified 2023-03 + a.Add("TXT", rejectif.TxtLongerThan(1000)) // Last verified 2023-12 a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified 2023-03 diff --git a/providers/dnsimple/dnsimpleProvider.go b/providers/dnsimple/dnsimpleProvider.go index b3d2b195f..7cade6918 100644 --- a/providers/dnsimple/dnsimpleProvider.go +++ b/providers/dnsimple/dnsimpleProvider.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "os" "sort" "strconv" "strings" @@ -12,6 +13,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" dnsimpleapi "github.com/dnsimple/dnsimple-go/dnsimple" "golang.org/x/oauth2" @@ -96,7 +98,14 @@ func (c *dnsimpleProvider) GetZoneRecords(domain string, meta map[string]string) // 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 ") { + if r.Type == "TXT" && strings.HasPrefix(r.Content, `"ALIAS for `) { + continue + } + // This second check is the same of before, but it exists for compatibility purpose. + // Until Nov 2023 DNSimple did not normalize TXT records, and they used to store TXT records without quotes. + // + // This is a backward-compatible function to facilitate the TXT transition. + if r.Type == "TXT" && strings.HasPrefix(r.Content, `ALIAS for `) { continue } @@ -120,7 +129,12 @@ func (c *dnsimpleProvider) GetZoneRecords(domain string, meta map[string]string) case "SRV": err = rec.SetTargetSRVPriorityString(uint16(r.Priority), r.Content) case "TXT": - err = rec.SetTargetTXT(r.Content) + // This is a backward-compatible function to facilitate the TXT transition. + if isQuotedTXT(r.Content) { + err = rec.PopulateFromStringFunc(r.Type, r.Content, domain, txtutil.ParseQuoted) + } else { + err = rec.SetTargetTXT(fmt.Sprintf("legacy: %s", r.Content)) + } default: err = rec.PopulateFromString(r.Type, r.Content, domain) } @@ -255,6 +269,10 @@ func (c *dnsimpleProvider) getDNSSECCorrections(dc *models.DomainConfig) ([]*mod // DNSimple calls +// Initializes a new DNSimple API client. +// +// - if BaseURL is present, the provided BaseURL is used. Useful to switch to DNSimple sandbox site. It defaults to production otherwise. +// - if "DNSIMPLE_DEBUG_HTTP" is set to "1", it enables the API client logging. func (c *dnsimpleProvider) getClient() *dnsimpleapi.Client { ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.AccountToken}) tc := oauth2.NewClient(context.Background(), ts) @@ -266,6 +284,9 @@ func (c *dnsimpleProvider) getClient() *dnsimpleapi.Client { if c.BaseURL != "" { client.BaseURL = c.BaseURL } + if os.Getenv("DNSIMPLE_DEBUG_HTTP") == "1" { + client.Debug = true + } return client } @@ -632,8 +653,10 @@ func newProvider(m map[string]string, _ json.RawMessage) (*dnsimpleProvider, err return api, nil } -// remove all non-dnsimple NS records from our desired state. -// if any are found, print a warning +// utilities + +// Removes all non-dnsimple NS records from our desired state. +// If any are found, print a warning. func removeOtherApexNS(dc *models.DomainConfig) { newList := make([]*models.RecordConfig, 0, len(dc.Records)) for _, rec := range dc.Records { @@ -653,7 +676,7 @@ func removeOtherApexNS(dc *models.DomainConfig) { dc.Records = newList } -// Return the correct combined content for all special record types, Target for everything else +// Returns the correct combined content for all special record types, Target for everything else // Using RecordConfig.GetTargetCombined returns priority in the string, which we do not allow func getTargetRecordContent(rc *models.RecordConfig) string { switch rtype := rc.Type; rtype { @@ -670,13 +693,13 @@ func getTargetRecordContent(rc *models.RecordConfig) string { case "SRV": return fmt.Sprintf("%d %d %s", rc.SrvWeight, rc.SrvPort, rc.GetTargetField()) case "TXT": - return rc.GetTargetTXTJoined() + return rc.GetTargetCombinedFunc(txtutil.EncodeQuoted) default: return rc.GetTargetField() } } -// Return the correct priority for the record type, 0 for records without priority +// Returns the correct priority for the record type, 0 for records without priority func getTargetRecordPriority(rc *models.RecordConfig) int { switch rtype := rc.Type; rtype { case "MX": @@ -711,3 +734,11 @@ func isDnsimpleNameServerDomain(name string) bool { } return false } + +// Tests if the content is encoded, performing a naive check on the presence of quotes +// at the beginning and end of the string. +// +// This is a backward-compatible function to facilitate the TXT transition. +func isQuotedTXT(content string) bool { + return content[0:1] == `"` && content[len(content)-1:] == `"` +}