1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

Re-engineer TXT records for simplicity and better compliance (#1063)

TXT records are now handled different.

1. The raw input from dnsconfig.js is passed all the way to the provider. The provider can determine if it can or can't handle such records (auditrecords.go) and processes them internally as such.
2. The CanUseTXTMulti capability is no longer needed.

* DSPs now register a table of functions
* Use audits for txt record variations
* unit tests pass. integration fails.
* fix deepcopy problem
* rename to AuditRecordSupport
* Reduce use of TXTMulti
* Remove CanUseTXTMulti
* fix Test Skip
* fix DO
* fix vultr
* fix NDC
* msdns fixes
* Fix powerdns and cloudflare
* HEDNS: Fix usage of target field to resolve TXT handling (#1067)
* Fix HEXONET

Co-authored-by: Robert Blenkinsopp <robert@blenkinsopp.net>
Co-authored-by: Jakob Ackermann <das7pad@outlook.com>
This commit is contained in:
Tom Limoncelli
2021-03-07 13:19:22 -05:00
committed by GitHub
parent 56766f93a9
commit 8dea9edc34
96 changed files with 162611 additions and 828 deletions

View File

@ -74,7 +74,7 @@ func generateFeatureMatrix() error {
} }
} }
setDoc("Official Support", providers.DocOfficiallySupported, true) setDoc("Official Support", providers.DocOfficiallySupported, true)
fm.SetSimple("DNS Provider", false, func() bool { return providers.DNSProviderTypes[p] != nil }) fm.SetSimple("DNS Provider", false, func() bool { return providers.DNSProviderTypes[p].Initializer != nil })
fm.SetSimple("Registrar", false, func() bool { return providers.RegistrarTypes[p] != nil }) fm.SetSimple("Registrar", false, func() bool { return providers.RegistrarTypes[p] != nil })
setCap("ALIAS", providers.CanUseAlias) setCap("ALIAS", providers.CanUseAlias)
setCap("AUTODNSSEC", providers.CanAutoDNSSEC) setCap("AUTODNSSEC", providers.CanAutoDNSSEC)
@ -86,7 +86,6 @@ func generateFeatureMatrix() error {
setCap("SRV", providers.CanUseSRV) setCap("SRV", providers.CanUseSRV)
setCap("SSHFP", providers.CanUseSSHFP) setCap("SSHFP", providers.CanUseSSHFP)
setCap("TLSA", providers.CanUseTLSA) setCap("TLSA", providers.CanUseTLSA)
setCap("TXTMulti", providers.CanUseTXTMulti)
setCap("get-zones", providers.CanGetZones) setCap("get-zones", providers.CanGetZones)
setCap("DS", providers.CanUseDS) setCap("DS", providers.CanUseDS)
setDoc("dual host", providers.DocDualHost, false) setDoc("dual host", providers.DocDualHost, false)

View File

@ -132,6 +132,9 @@ DomainLoop:
if !shouldrun { if !shouldrun {
continue continue
} }
/// This is where we should audit?
corrections, err := provider.Driver.GetDomainCorrections(dc) corrections, err := provider.Driver.GetDomainCorrections(dc)
out.EndProvider(len(corrections), err) out.EndProvider(len(corrections), err)
if err != nil { if err != nil {

View File

@ -939,78 +939,36 @@
<tr> <tr>
<th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider can manage TXT records with multiple strings">TXTMulti</th> <th class="row-header" style="text-decoration: underline;" data-toggle="tooltip" data-container="body" data-placement="top" title="Provider can manage TXT records with multiple strings">TXTMulti</th>
<td><i class="fa fa-minus dim"></i></td> <td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td> <td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td> <td><i class="fa fa-minus dim"></i></td>
<td class="success" data-toggle="tooltip" data-container="body" data-placement="top" title="A broken parser prevents TXTMulti strings from including double-quotes; The total length of all strings can&#39;t be longer than 512; and in reality must be shorter due to sloppy validation checks.">
<a href="https://github.com/StackExchange/dnscontrol/issues/370"><i class="fa has-tooltip fa-check text-success" aria-hidden="true"></i></a>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td> <td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td> <td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="info">
<i class="fa fa-circle-o text-info" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td class="success">
<i class="fa fa-check text-success" aria-hidden="true"></i>
</td>
<td class="danger">
<i class="fa fa-times text-danger" aria-hidden="true"></i>
</td>
<td><i class="fa fa-minus dim"></i></td> <td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td> <td><i class="fa fa-minus dim"></i></td>
<td class="success"> <td><i class="fa fa-minus dim"></i></td>
<i class="fa fa-check text-success" aria-hidden="true"></i> <td><i class="fa fa-minus dim"></i></td>
</td> <td><i class="fa fa-minus dim"></i></td>
<td class="success"> <td><i class="fa fa-minus dim"></i></td>
<i class="fa fa-check text-success" aria-hidden="true"></i> <td><i class="fa fa-minus dim"></i></td>
</td> <td><i class="fa fa-minus dim"></i></td>
<td class="success"> <td><i class="fa fa-minus dim"></i></td>
<i class="fa fa-check text-success" aria-hidden="true"></i> <td><i class="fa fa-minus dim"></i></td>
</td> <td><i class="fa fa-minus dim"></i></td>
<td class="success"> <td><i class="fa fa-minus dim"></i></td>
<i class="fa fa-check text-success" aria-hidden="true"></i> <td><i class="fa fa-minus dim"></i></td>
</td> <td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td> <td><i class="fa fa-minus dim"></i></td>
<td><i class="fa fa-minus dim"></i></td> <td><i class="fa fa-minus dim"></i></td>
</tr> </tr>

View File

@ -40,6 +40,4 @@ D("example.tld", REG_NONE, DnsProvider(DIGITALOCEAN),
- Digitalocean DNS doesn't support `;` value with CAA-records ([DigitalOcean documentation](https://www.digitalocean.com/docs/networking/dns/how-to/create-caa-records/)) - Digitalocean DNS doesn't support `;` value with CAA-records ([DigitalOcean documentation](https://www.digitalocean.com/docs/networking/dns/how-to/create-caa-records/))
- While Digitalocean DNS supports TXT records with multiple strings, - While Digitalocean DNS supports TXT records with multiple strings,
their implementation is lacking. It does not support strings that their length is limited by the max API request of 512 octets.
include double-quotes nor many long strings. The length limits may
restrict your ability to use very long DKIM or SPF records.

View File

@ -65,7 +65,7 @@ provider that does not support the capability.
* Add the capability to the validations in `pkg/normalize/validate.go` * Add the capability to the validations in `pkg/normalize/validate.go`
by adding it to `providerCapabilityChecks` by adding it to `providerCapabilityChecks`
* Some capabilities can't be tested for, such as `CanUseTXTMulti`. If * Some capabilities can't be tested for. If
such testing can't be done, add it to the whitelist in function such testing can't be done, add it to the whitelist in function
`TestCapabilitiesAreFiltered` in `TestCapabilitiesAreFiltered` in
`pkg/normalize/capabilities_test.go` `pkg/normalize/capabilities_test.go`
@ -148,7 +148,7 @@ testgroup("MX", <<< 1
tc("MX record", <<< 4 tc("MX record", <<< 4
mx("@", 10, "foo.com."), mx("@", 10, "foo.com."),
mx("@", 20, "bar.com."), mx("@", 20, "bar.com."),
), ),
) )
``` ```
@ -156,7 +156,7 @@ Line 1: `testgroup()` gives a name to a group of tests. It also tells
the system to delete all records for this domain so that the tests the system to delete all records for this domain so that the tests
begin with a blank slate. begin with a blank slate.
Line 2: Line 2:
Each `tc()` encodes all the records of a zone. The test framework Each `tc()` encodes all the records of a zone. The test framework
will try to do the smallest changes to bring the zone up to date. will try to do the smallest changes to bring the zone up to date.
In this case, we know the zone is empty, so this will add one MX In this case, we know the zone is empty, so this will add one MX

7
go.mod
View File

@ -43,7 +43,7 @@ require (
github.com/hexonet/go-sdk v3.5.1+incompatible github.com/hexonet/go-sdk v3.5.1+incompatible
github.com/jarcoal/httpmock v1.0.8 // indirect github.com/jarcoal/httpmock v1.0.8 // indirect
github.com/jinzhu/copier v0.2.5 github.com/jinzhu/copier v0.2.5
github.com/juju/testing v0.0.0-20201216035041-2be42bba85f3 // indirect github.com/juju/errors v0.0.0-20200330140219-3fe23663418f // indirect
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b // indirect github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b // indirect
github.com/miekg/dns v1.1.35 github.com/miekg/dns v1.1.35
github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect
@ -66,9 +66,8 @@ require (
github.com/softlayer/softlayer-go v0.0.0-20170804160555-5e1c8cccc730 github.com/softlayer/softlayer-go v0.0.0-20170804160555-5e1c8cccc730
github.com/stretchr/objx v0.3.0 // indirect github.com/stretchr/objx v0.3.0 // indirect
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/tdewolff/minify v2.3.6+incompatible github.com/tdewolff/minify v2.3.6+incompatible // indirect
github.com/tdewolff/parse v2.3.4+incompatible // indirect github.com/tdewolff/parse v2.3.4+incompatible // indirect
github.com/tdewolff/test v1.0.6 // indirect
github.com/urfave/cli/v2 v2.3.0 github.com/urfave/cli/v2 v2.3.0
github.com/vultr/govultr v1.0.0 github.com/vultr/govultr v1.0.0
github.com/xddxdd/ottoext v0.0.0-20210101073831-439879ee6281 github.com/xddxdd/ottoext v0.0.0-20210101073831-439879ee6281
@ -85,6 +84,6 @@ require (
gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.4.3 gopkg.in/ns1/ns1-go.v2 v2.4.3
gopkg.in/square/go-jose.v2 v2.5.1 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
) )

33
go.sum
View File

@ -278,31 +278,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/ansiterm v0.0.0-20160907234532-b99631de12cf/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA=
github.com/juju/cmd v0.0.0-20171107070456-e74f39857ca0/go.mod h1:yWJQHl73rdSX4DHVKGqkAip+huBslxRwS8m9CrOLq18=
github.com/juju/collections v0.0.0-20200605021417-0d0ec82b7271/go.mod h1:5XgO71dV1JClcOJE+4dzdn4HrI5LiyKd7PlVG6eZYhY=
github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/errors v0.0.0-20200330140219-3fe23663418f h1:MCOvExGLpaSIzLYB4iQXEHP4jYVU6vmzLNQPdMVrxnM= github.com/juju/errors v0.0.0-20200330140219-3fe23663418f h1:MCOvExGLpaSIzLYB4iQXEHP4jYVU6vmzLNQPdMVrxnM=
github.com/juju/errors v0.0.0-20200330140219-3fe23663418f/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/errors v0.0.0-20200330140219-3fe23663418f/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/juju/httpprof v0.0.0-20141217160036-14bf14c30767/go.mod h1:+MaLYz4PumRkkyHYeXJ2G5g5cIW0sli2bOfpmbaMV/g=
github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/loggo v0.0.0-20200526014432-9ce3a2e09b5e h1:FdDd7bdI6cjq5vaoYlK1mfQYfF9sF2VZw8VEZMsl5t8=
github.com/juju/loggo v0.0.0-20200526014432-9ce3a2e09b5e/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/mutex v0.0.0-20171110020013-1fe2a4bf0a3a/go.mod h1:Y3oOzHH8CQ0Ppt0oCKJ2JFO81/EsWenH5AEqigLH+yY=
github.com/juju/retry v0.0.0-20151029024821-62c620325291/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4=
github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4=
github.com/juju/testing v0.0.0-20180402130637-44801989f0f7/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
github.com/juju/testing v0.0.0-20201216035041-2be42bba85f3 h1:ram7cW6jDPTu6cv9xDMwa+tO7RsO4BdsubxrJ4EEw+E=
github.com/juju/testing v0.0.0-20201216035041-2be42bba85f3/go.mod h1:IbSKFoKW0bzmbDZ7rBwF/L3lO3b1bpmOIhTXQl/WJxw=
github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk=
github.com/juju/utils v0.0.0-20200116185830-d40c2fe10647/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk=
github.com/juju/utils/v2 v2.0.0-20200923005554-4646bfea2ef1/go.mod h1:fdlDtQlzundleLLz/ggoYinEt/LmnrpNKcNTABQATNI=
github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
github.com/juju/version v0.0.0-20191219164919-81c1be00b9a6/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
github.com/julienschmidt/httprouter v1.1.1-0.20151013225520-77a895ad01eb/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.1.1-0.20151013225520-77a895ad01eb/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ= github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
@ -346,8 +323,6 @@ github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAA
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/nrdcg/goinwx v0.8.1 h1:20EQ/JaGFnSKwiDH2JzjIpicffl3cPk6imJBDqVBVtU= github.com/nrdcg/goinwx v0.8.1 h1:20EQ/JaGFnSKwiDH2JzjIpicffl3cPk6imJBDqVBVtU=
github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c= github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c=
@ -408,8 +383,6 @@ github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9
github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs= github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs=
github.com/tdewolff/parse v2.3.4+incompatible h1:x05/cnGwIMf4ceLuDMBOdQ1qGniMoxpP46ghf0Qzh38= github.com/tdewolff/parse v2.3.4+incompatible h1:x05/cnGwIMf4ceLuDMBOdQ1qGniMoxpP46ghf0Qzh38=
github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ= github.com/tdewolff/parse v2.3.4+incompatible/go.mod h1:8oBwCsVmUkgHO8M5iCzSIDtpzXOT0WXX9cWhz+bIzJQ=
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
@ -746,8 +719,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v1 v1.0.0-20161222125816-442357a80af5/go.mod h1:u0ALmqvLRxLI95fkdCEWrE6mhWYZW1aMOJHp5YXLHTg= gopkg.in/errgo.v1 v1.0.0-20161222125816-442357a80af5/go.mod h1:u0ALmqvLRxLI95fkdCEWrE6mhWYZW1aMOJHp5YXLHTg=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/h2non/gock.v1 v1.0.14 h1:fTeu9fcUvSnLNacYvYI54h+1/XEteDyHvrVCZEEEYNM= gopkg.in/h2non/gock.v1 v1.0.14 h1:fTeu9fcUvSnLNacYvYI54h+1/XEteDyHvrVCZEEEYNM=
@ -756,7 +729,6 @@ gopkg.in/httprequest.v1 v1.1.1/go.mod h1:/CkavNL+g3qLOrpFHVrEx4NKepeqR4XTZWNj4sG
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/ns1/ns1-go.v2 v2.4.3 h1:f7oKEJYSMD2TfR8RDeN1CT2ZsNahTPJEQfxOpmMCwvk= gopkg.in/ns1/ns1-go.v2 v2.4.3 h1:f7oKEJYSMD2TfR8RDeN1CT2ZsNahTPJEQfxOpmMCwvk=
gopkg.in/ns1/ns1-go.v2 v2.4.3/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk= gopkg.in/ns1/ns1-go.v2 v2.4.3/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk=
@ -767,6 +739,7 @@ gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -180,12 +180,17 @@ func makeChanges(t *testing.T, prv providers.DNSServiceProvider, dc *models.Doma
models.PostProcessRecords(dom.Records) models.PostProcessRecords(dom.Records)
dom2, _ := dom.Copy() dom2, _ := dom.Copy()
if err := providers.AuditRecords(*providerToRun, dom.Records); err != nil {
t.Skip(fmt.Sprintf("***SKIPPED(PROVIDER DOES NOT SUPPORT '%s' ::%q)", err, desc))
return
}
// get and run corrections for first time // get and run corrections for first time
corrections, err := prv.GetDomainCorrections(dom) corrections, err := prv.GetDomainCorrections(dom)
if err != nil { if err != nil {
t.Fatal(fmt.Errorf("runTests: %w", err)) t.Fatal(fmt.Errorf("runTests: %w", err))
} }
if len(corrections) == 0 && expectChanges { if (len(corrections) == 0 && expectChanges) && (tst.Desc != "Empty") {
t.Fatalf("Expected changes, but got none") t.Fatalf("Expected changes, but got none")
} }
for _, c := range corrections { for _, c := range corrections {
@ -386,64 +391,49 @@ func ptr(name, target string) *models.RecordConfig {
func naptr(name string, order uint16, preference uint16, flags string, service string, regexp string, target string) *models.RecordConfig { func naptr(name string, order uint16, preference uint16, flags string, service string, regexp string, target string) *models.RecordConfig {
r := makeRec(name, target, "NAPTR") r := makeRec(name, target, "NAPTR")
r.NaptrOrder = order r.SetTargetNAPTR(order, preference, flags, service, regexp, target)
r.NaptrPreference = preference
r.NaptrFlags = flags
r.NaptrService = service
r.NaptrRegexp = regexp
return r return r
} }
func ds(name string, keyTag uint16, algorithm, digestType uint8, digest string) *models.RecordConfig { func ds(name string, keyTag uint16, algorithm, digestType uint8, digest string) *models.RecordConfig {
r := makeRec(name, "", "DS") r := makeRec(name, "", "DS")
r.DsKeyTag = keyTag r.SetTargetDS(keyTag, algorithm, digestType, digest)
r.DsAlgorithm = algorithm
r.DsDigestType = digestType
r.DsDigest = digest
return r return r
} }
func srv(name string, priority, weight, port uint16, target string) *models.RecordConfig { func srv(name string, priority, weight, port uint16, target string) *models.RecordConfig {
r := makeRec(name, target, "SRV") r := makeRec(name, target, "SRV")
r.SrvPriority = priority r.SetTargetSRV(priority, weight, port, target)
r.SrvWeight = weight
r.SrvPort = port
return r return r
} }
func sshfp(name string, algorithm uint8, fingerprint uint8, target string) *models.RecordConfig { func sshfp(name string, algorithm uint8, fingerprint uint8, target string) *models.RecordConfig {
r := makeRec(name, target, "SSHFP") r := makeRec(name, target, "SSHFP")
r.SshfpAlgorithm = algorithm r.SetTargetSSHFP(algorithm, fingerprint, target)
r.SshfpFingerprint = fingerprint
return r return r
} }
func txt(name, target string) *models.RecordConfig { func txt(name, target string) *models.RecordConfig {
// FYI: This must match the algorithm in pkg/js/helpers.js TXT. r := makeRec(name, "", "TXT")
r := makeRec(name, target, "TXT") r.SetTargetTXT(target)
r.TxtStrings = []string{target}
return r return r
} }
func txtmulti(name string, target []string) *models.RecordConfig { func txtmulti(name string, target []string) *models.RecordConfig {
// FYI: This must match the algorithm in pkg/js/helpers.js TXT. r := makeRec(name, "", "TXT")
r := makeRec(name, target[0], "TXT") r.SetTargetTXTs(target)
r.TxtStrings = target
return r return r
} }
func caa(name string, tag string, flag uint8, target string) *models.RecordConfig { func caa(name string, tag string, flag uint8, target string) *models.RecordConfig {
r := makeRec(name, target, "CAA") r := makeRec(name, target, "CAA")
r.CaaFlag = flag r.SetTargetCAA(flag, tag, target)
r.CaaTag = tag
return r return r
} }
func tlsa(name string, usage, selector, matchingtype uint8, target string) *models.RecordConfig { func tlsa(name string, usage, selector, matchingtype uint8, target string) *models.RecordConfig {
r := makeRec(name, target, "TLSA") r := makeRec(name, target, "TLSA")
r.TlsaUsage = usage r.SetTargetTLSA(usage, selector, matchingtype, target)
r.TlsaSelector = selector
r.TlsaMatchingType = matchingtype
return r return r
} }
@ -480,6 +470,29 @@ func ttl(r *models.RecordConfig, t uint32) *models.RecordConfig {
return r return r
} }
func gentxt(s string) *TestCase {
title := fmt.Sprintf("Create TXT %s", s)
label := fmt.Sprintf("foo%d", len(s))
l := []string{}
for _, j := range s {
switch j {
case '0', 's':
//title += " short"
label += "s"
l = append(l, "short")
case 'h':
//title += " 128"
label += "h"
l = append(l, strings.Repeat("H", 128))
case '1', 'l':
//title += " 255"
label += "l"
l = append(l, strings.Repeat("Z", 255))
}
}
return tc(title, txtmulti(label, l))
}
func manyA(namePattern, target string, n int) []*models.RecordConfig { func manyA(namePattern, target string, n int) []*models.RecordConfig {
recs := []*models.RecordConfig{} recs := []*models.RecordConfig{}
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
@ -684,10 +697,11 @@ func makeTests(t *testing.T) []*TestGroup {
), ),
testgroup("NS", testgroup("NS",
not("DNSIMPLE", "EXOSCALE", "NETCUP"), not(
// DNSIMPLE: Does not support NS records nor subdomains. "DNSIMPLE", // Does not support NS records nor subdomains.
// EXOSCALE: FILL IN "EXOSCALE", // Not supported.
// Netcup: NS records not currently supported. "NETCUP", // NS records not currently supported.
),
tc("NS for subdomain", ns("xyz", "ns2.foo.com.")), tc("NS for subdomain", ns("xyz", "ns2.foo.com.")),
tc("Dual NS for subdomain", ns("xyz", "ns2.foo.com."), ns("xyz", "ns1.foo.com.")), tc("Dual NS for subdomain", ns("xyz", "ns2.foo.com."), ns("xyz", "ns1.foo.com.")),
tc("NS Record pointing to @", a("@", "1.2.3.4"), ns("foo", "**current-domain**")), tc("NS Record pointing to @", a("@", "1.2.3.4"), ns("foo", "**current-domain**")),
@ -709,57 +723,142 @@ func makeTests(t *testing.T) []*TestGroup {
tc("Add a new record - ignoring **.foo.com. targets", a("bar", "1.2.3.4"), ignoreTarget("**.foo.com.", "CNAME")), tc("Add a new record - ignoring **.foo.com. targets", a("bar", "1.2.3.4"), ignoreTarget("**.foo.com.", "CNAME")),
), ),
testgroup("single TXT", testgroup("simple TXT",
tc("Create a TXT", txt("foo", "simple")), tc("Create a TXT", txt("foo", "simple")),
tc("Change a TXT", txt("foo", "changed")), tc("Change a TXT", txt("foo", "changed")),
clear(),
tc("Create a TXT with spaces", txt("foo", "with spaces")), tc("Create a TXT with spaces", txt("foo", "with spaces")),
tc("Create 1 TXT as array", txtmulti("foo", []string{"simple"})), // Same as non-TXTMulti
clear(),
tc("Create a 254-byte TXT", txt("foo", strings.Repeat("A", 254))),
), ),
testgroup("max-sized TXT", testgroup("long TXT",
not( tc("Create long TXT", txt("foo", strings.Repeat("A", 300))),
"INWX", // INWX does not support tc("Change long TXT", txt("foo", strings.Repeat("B", 310))),
"MSDNS", // Supports 254-byte strings, not 255. Seems like a bug. tc("Create long TXT with spaces", txt("foo", strings.Repeat("X", 200)+" "+strings.Repeat("Y", 200))),
),
tc("Create a 255-byte TXT", txt("foo", strings.Repeat("A", 255))),
), ),
testgroup("single TXT with single-quote", // In this next section we test all the edge cases related to TXT
not( // records. Compliance with the RFCs varies greatly with each provider.
"INWX", // Bug in the API prevents this. // Rather than creating a "Capability" for each possible different
"MSDNS", // TODO(tlim): Should be easy to implement. // failing or malcompliance (there would be many!), each provider
"CLOUDNS", // support txt("foo", "blah'blah") but does not support txt("foo","blah`blah") // supplies a function AuditRecords() which returns an error if
), // the provider can not support a record.
tc("Create TXT with single-quote", txt("foo", "blah`blah")), // The integration tests use this feedback to skip tests that we know would fail.
), // (Elsewhere the result of AuditRecords() is used in the
// "dnscontrol check" phase.)
testgroup("ws TXT", testgroup("complex TXT",
not("CLOUDFLAREAPI", "HEXONET", "INWX", "NAMEDOTCOM", "CLOUDNS"), // Do not use only()/not()/requires() in this section.
// These providers strip whitespace at the end of TXT records. // If your provider needs to skip one of these tests, update
// TODO(tal): Add a check for this in normalize/validate.go // "provider/*/recordaudit.AuditRecords()" to reject that kind
tc("Change a TXT with ws at end", txt("foo", "with space at end ")), // of record. When the provider fixes the bug or changes behavior,
), // update the AuditRecords().
tc("TXT with 0-octel string", txt("foo1", "")),
testgroup("empty TXT",
not(
"HETZNER", // Not supported.
"HEXONET", // Not supported.
"INWX", // Not supported.
"MSDNS", // Not supported.
"NETCUP", // Not supported.
"CLOUDNS", // Not supported.
),
tc("TXT with empty str", txt("foo1", "")),
// https://github.com/StackExchange/dnscontrol/issues/598 // https://github.com/StackExchange/dnscontrol/issues/598
// We decided that permitting the TXT target to be an empty // RFC1035 permits this, but rarely do provider support it.
// string is not a requirement (even though RFC1035 permits it). clear(),
// In the future we might make it "capability" to tc("Create a 253-byte TXT", txt("foo253", strings.Repeat("A", 253))),
// indicate which vendors support an empty TXT record. clear(),
// However at this time there is no pressing need for this tc("Create a 254-byte TXT", txt("foo254", strings.Repeat("B", 254))),
// feature. clear(),
tc("Create a 255-byte TXT", txt("foo255", strings.Repeat("C", 255))),
clear(),
tc("Create a 256-byte TXT", txt("foo256", strings.Repeat("D", 256))),
clear(),
tc("Create a 257-byte TXT", txt("foo257", strings.Repeat("E", 257))),
clear(),
tc("Create TXT with single-quote", txt("foosq", "quo'te")),
clear(),
tc("Create TXT with backtick", txt("foobt", "blah`blah")),
clear(),
tc("Create TXT with double-quote", txt("foodq", `quo"te`)),
clear(),
tc("Create TXT with ws at end", txt("foows1", "with space at end ")),
clear(), gentxt("0"),
clear(), gentxt("1"),
clear(), gentxt("10"),
clear(), gentxt("11"),
clear(), gentxt("100"),
clear(), gentxt("101"),
clear(), gentxt("110"),
clear(), gentxt("111"),
clear(), gentxt("1hh"),
clear(), gentxt("1hh0"),
),
testgroup("long TXT",
tc("Create a 505 TXT", txt("foo257", strings.Repeat("E", 505))),
tc("Create a 506 TXT", txt("foo257", strings.Repeat("E", 506))),
tc("Create a 507 TXT", txt("foo257", strings.Repeat("E", 507))),
tc("Create a 508 TXT", txt("foo257", strings.Repeat("E", 508))),
tc("Create a 509 TXT", txt("foo257", strings.Repeat("E", 509))),
tc("Create a 510 TXT", txt("foo257", strings.Repeat("E", 510))),
tc("Create a 511 TXT", txt("foo257", strings.Repeat("E", 511))),
tc("Create a 512 TXT", txt("foo257", strings.Repeat("E", 512))),
tc("Create a 513 TXT", txt("foo257", strings.Repeat("E", 513))),
tc("Create a 514 TXT", txt("foo257", strings.Repeat("E", 514))),
tc("Create a 515 TXT", txt("foo257", strings.Repeat("E", 515))),
tc("Create a 516 TXT", txt("foo257", strings.Repeat("E", 516))),
),
// Test the ability to change TXT records on the DIFFERENT labels accurately.
testgroup("TXTMulti",
tc("Create TXTMulti 1",
txtmulti("foo1", []string{"simple"}),
),
tc("Add TXTMulti 2",
txtmulti("foo1", []string{"simple"}),
txtmulti("foo2", []string{"one", "two"}),
),
tc("Add TXTMulti 3",
txtmulti("foo1", []string{"simple"}),
txtmulti("foo2", []string{"one", "two"}),
txtmulti("foo3", []string{"eh", "bee", "cee"}),
),
tc("Change TXTMultii-0",
txtmulti("foo1", []string{"dimple"}),
txtmulti("foo2", []string{"fun", "two"}),
txtmulti("foo3", []string{"eh", "bzz", "cee"}),
),
tc("Change TXTMulti-1[0]",
txtmulti("foo1", []string{"dimple"}),
txtmulti("foo2", []string{"moja", "two"}),
txtmulti("foo3", []string{"eh", "bzz", "cee"}),
),
tc("Change TXTMulti-1[1]",
txtmulti("foo1", []string{"dimple"}),
txtmulti("foo2", []string{"moja", "mbili"}),
txtmulti("foo3", []string{"eh", "bzz", "cee"}),
),
),
// Test the ability to change TXT records on the SAME labels accurately.
testgroup("TXTMulti",
tc("Create TXTMulti 1",
txtmulti("foo", []string{"simple"}),
),
tc("Add TXTMulti 2",
txtmulti("foo", []string{"simple"}),
txtmulti("foo", []string{"one", "two"}),
),
tc("Add TXTMulti 3",
txtmulti("foo", []string{"simple"}),
txtmulti("foo", []string{"one", "two"}),
txtmulti("foo", []string{"eh", "bee", "cee"}),
),
tc("Change TXTMultii-0",
txtmulti("foo", []string{"dimple"}),
txtmulti("foo", []string{"fun", "two"}),
txtmulti("foo", []string{"eh", "bzz", "cee"}),
),
tc("Change TXTMulti-1[0]",
txtmulti("foo", []string{"dimple"}),
txtmulti("foo", []string{"moja", "two"}),
txtmulti("foo", []string{"eh", "bzz", "cee"}),
),
tc("Change TXTMulti-1[1]",
txtmulti("foo", []string{"dimple"}),
txtmulti("foo", []string{"moja", "mbili"}),
txtmulti("foo", []string{"eh", "bzz", "cee"}),
),
), ),
// //
@ -920,69 +1019,6 @@ func makeTests(t *testing.T) []*TestGroup {
tc("TLSA change certificate", tlsa("_443._tcp", 2, 0, 2, reversedSha512)), tc("TLSA change certificate", tlsa("_443._tcp", 2, 0, 2, reversedSha512)),
), ),
testgroup("TXTMulti",
not("CLOUDNS"), //TODO: not implemented. same Issue as #996
requires(providers.CanUseTXTMulti),
tc("Create TXTMulti 1",
txtmulti("foo1", []string{"simple"}),
),
tc("Create TXTMulti 2",
txtmulti("foo1", []string{"simple"}),
txtmulti("foo2", []string{"one", "two"}),
),
tc("Create TXTMulti 3",
txtmulti("foo1", []string{"simple"}),
txtmulti("foo2", []string{"one", "two"}),
txtmulti("foo3", []string{"eh", "bee", "cee"}),
),
tc("Change TXTMulti",
txtmulti("foo1", []string{"dimple"}),
txtmulti("foo2", []string{"fun", "two"}),
txtmulti("foo3", []string{"eh", "bzz", "cee"}),
),
tc("Long TXTMulti",
// This isn't the longest a TXT record can be, but it is the
// longest DIGITALOCEAN permits.
txtmulti("foo4", []string{strings.Repeat("X", 255), strings.Repeat("Y", 252)}),
),
),
testgroup("TXTMulti tests that break DO",
// DO's implementation of TXTMulti is broken, thus we separate out
// tests that fail for it. Users are warned about these limits
// in docs/_providers/digitalocean.md
requires(providers.CanUseTXTMulti),
not("DIGITALOCEAN"),
// Digital Ocean's TXT record implementation checks size limits wrong.
// RFC 1035 Section 3.3.14 states that each substring can be 255
// octets, and there is no limit on the number of such
// substrings, aside from the usual packet length limits. DO's
// implementation restricts the total length to be 512 octets,
// including any backlashes used for escapes, quotes, and other
// metachars. In other words, they're doing the checking on the
// API protocol encoded data instead of on on the resulting TXT
// record.
// Proper TXT implementations can handle TXT records like this:
tc("3x255-byte TXTMulti",
txtmulti("foo3", []string{strings.Repeat("X", 255), strings.Repeat("Y", 255), strings.Repeat("Z", 255)})),
clear(),
// Digital Ocean's TXT record implementation handles quotes wrong.
// It craps out if your TXT record includes double-quotes.
// Someone doesn't understand how zonefile-style quoting is
// supposed to work.
// Proper TXT implementations can handle TXT records like these:
tc("Create TXTMulti with quotes",
txtmulti("foo1", []string{"simple"}),
txtmulti("foo2", []string{"o\"ne", "tw\"o"}),
txtmulti("foo3", []string{"eh", "bee", "cee"}),
),
tc("Change TXTMulti",
txtmulti("foo1", []string{"dimple"}),
txtmulti("foo2", []string{"fun", "t\"wo"}),
txtmulti("foo3", []string{"eh", "bzz", "cee"}),
),
),
testgroup("DS", testgroup("DS",
requires(providers.CanUseDS), requires(providers.CanUseDS),
tc("create DS", ds("@", 1, 13, 1, "ADIGEST")), tc("create DS", ds("@", 1, 13, 1, "ADIGEST")),

View File

@ -1,112 +0,0 @@
--- PASS: TestDNSProviders/example.com/0:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/1:_Create_an_A_record (0.00s)
--- PASS: TestDNSProviders/example.com/2:_Change_it (0.00s)
--- PASS: TestDNSProviders/example.com/3:_Add_another (0.00s)
--- PASS: TestDNSProviders/example.com/4:_Add_another(same_name) (0.00s)
--- PASS: TestDNSProviders/example.com/5:_Change_a_ttl (0.00s)
--- PASS: TestDNSProviders/example.com/6:_Change_single_target_from_set (0.00s)
--- PASS: TestDNSProviders/example.com/7:_Change_all_ttls (0.00s)
--- PASS: TestDNSProviders/example.com/8:_Delete_one (0.00s)
--- PASS: TestDNSProviders/example.com/9:_Add_back_and_change_ttl (0.00s)
--- PASS: TestDNSProviders/example.com/10:_Change_targets_and_ttls (0.00s)
--- PASS: TestDNSProviders/example.com/11:_Create_wildcard (0.00s)
--- PASS: TestDNSProviders/example.com/12:_Delete_wildcard (0.00s)
--- PASS: TestDNSProviders/example.com/13:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/14:_Create_a_CNAME (0.00s)
--- PASS: TestDNSProviders/example.com/15:_Change_it (0.00s)
--- PASS: TestDNSProviders/example.com/16:_Change_to_A_record (0.00s)
--- PASS: TestDNSProviders/example.com/17:_Change_back_to_CNAME (0.00s)
--- PASS: TestDNSProviders/example.com/18:_Record_pointing_to_@ (0.00s)
--- PASS: TestDNSProviders/example.com/19:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/20:_NS_for_subdomain (0.00s)
--- PASS: TestDNSProviders/example.com/21:_Dual_NS_for_subdomain (0.00s)
--- PASS: TestDNSProviders/example.com/22:_NS_Record_pointing_to_@ (0.00s)
--- PASS: TestDNSProviders/example.com/23:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/24:_Internationalized_name (0.00s)
--- PASS: TestDNSProviders/example.com/25:_Change_IDN (0.00s)
--- PASS: TestDNSProviders/example.com/26:_Internationalized_CNAME_Target (0.00s)
--- PASS: TestDNSProviders/example.com/27:_IDN_CNAME_AND_Target (0.00s)
--- PASS: TestDNSProviders/example.com/28:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/29:_MX_record (0.00s)
--- PASS: TestDNSProviders/example.com/30:_Second_MX_record,_same_prio (0.00s)
--- PASS: TestDNSProviders/example.com/31:_3_MX (0.00s)
--- PASS: TestDNSProviders/example.com/32:_Delete_one (0.00s)
--- PASS: TestDNSProviders/example.com/33:_Change_to_other_name (0.00s)
--- PASS: TestDNSProviders/example.com/34:_Change_Preference (0.00s)
--- PASS: TestDNSProviders/example.com/35:_Record_pointing_to_@ (0.00s)
--- PASS: TestDNSProviders/example.com/36:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/37:_Create_PTR_record (0.00s)
--- PASS: TestDNSProviders/example.com/38:_Modify_PTR_record (0.00s)
--- PASS: TestDNSProviders/example.com/39:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/40:_NAPTR_record (0.00s)
--- PASS: TestDNSProviders/example.com/41:_NAPTR_second_record (0.00s)
--- PASS: TestDNSProviders/example.com/42:_NAPTR_delete_record (0.00s)
--- PASS: TestDNSProviders/example.com/43:_NAPTR_change_target (0.00s)
--- PASS: TestDNSProviders/example.com/44:_NAPTR_change_order (0.00s)
--- PASS: TestDNSProviders/example.com/45:_NAPTR_change_preference (0.00s)
--- PASS: TestDNSProviders/example.com/46:_NAPTR_change_flags (0.00s)
--- PASS: TestDNSProviders/example.com/47:_NAPTR_change_service (0.00s)
--- PASS: TestDNSProviders/example.com/48:_NAPTR_change_regexp (0.00s)
--- PASS: TestDNSProviders/example.com/49:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/50:_SRV_record (0.00s)
--- PASS: TestDNSProviders/example.com/51:_Second_SRV_record,_same_prio (0.00s)
--- PASS: TestDNSProviders/example.com/52:_3_SRV (0.00s)
--- PASS: TestDNSProviders/example.com/53:_Delete_one (0.00s)
--- PASS: TestDNSProviders/example.com/54:_Change_Target (0.00s)
--- PASS: TestDNSProviders/example.com/55:_Change_Priority (0.00s)
--- PASS: TestDNSProviders/example.com/56:_Change_Weight (0.00s)
--- PASS: TestDNSProviders/example.com/57:_Change_Port (0.00s)
--- PASS: TestDNSProviders/example.com/58:_Null_Target (0.00s)
--- PASS: TestDNSProviders/example.com/59:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/60:_SSHFP_record (0.00s)
--- PASS: TestDNSProviders/example.com/61:_SSHFP_change_algorithm (0.00s)
--- PASS: TestDNSProviders/example.com/62:_SSHFP_change_fingerprint_and_type (0.00s)
--- PASS: TestDNSProviders/example.com/63:_SSHFP_Delete_one (0.00s)
--- PASS: TestDNSProviders/example.com/64:_SSHFP_add_many_records (0.00s)
--- PASS: TestDNSProviders/example.com/65:_SSHFP_delete_two (0.00s)
--- PASS: TestDNSProviders/example.com/66:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/67:_CAA_record (0.00s)
--- PASS: TestDNSProviders/example.com/68:_CAA_change_tag (0.00s)
--- PASS: TestDNSProviders/example.com/69:_CAA_change_target (0.00s)
--- PASS: TestDNSProviders/example.com/70:_CAA_change_flag (0.00s)
--- PASS: TestDNSProviders/example.com/71:_CAA_many_records (0.00s)
--- PASS: TestDNSProviders/example.com/72:_CAA_delete (0.00s)
--- PASS: TestDNSProviders/example.com/73:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/74:_TLSA_record (0.00s)
--- PASS: TestDNSProviders/example.com/75:_TLSA_change_usage (0.00s)
--- PASS: TestDNSProviders/example.com/76:_TLSA_change_selector (0.00s)
--- PASS: TestDNSProviders/example.com/77:_TLSA_change_matchingtype (0.00s)
--- PASS: TestDNSProviders/example.com/78:_TLSA_change_certificate (0.00s)
--- PASS: TestDNSProviders/example.com/79:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/80:_Create_CAPS (0.00s)
--- PASS: TestDNSProviders/example.com/81:_Downcase_label (0.00s)
--- PASS: TestDNSProviders/example.com/82:_Downcase_target (0.00s)
--- PASS: TestDNSProviders/example.com/83:_Upcase_both (0.00s)
--- PASS: TestDNSProviders/example.com/84:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/85:_99_records (0.01s)
--- PASS: TestDNSProviders/example.com/86:_100_records (0.01s)
--- PASS: TestDNSProviders/example.com/87:_101_records (0.03s)
--- PASS: TestDNSProviders/example.com/88:_Empty (0.01s)
--- PASS: TestDNSProviders/example.com/89:_Create_a_TXT (0.03s)
--- PASS: TestDNSProviders/example.com/90:_Change_a_TXT (0.00s)
--- PASS: TestDNSProviders/example.com/91:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/92:_Create_a_TXT_with_spaces (0.00s)
--- PASS: TestDNSProviders/example.com/93:_Change_a_TXT_with_spaces (0.00s)
--- PASS: TestDNSProviders/example.com/94:_Create_1_TXT_as_array (0.01s)
--- PASS: TestDNSProviders/example.com/95:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/96:_Create_a_255-byte_TXT (0.00s)
--- PASS: TestDNSProviders/example.com/97:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/98:_Create_TXTMulti_1 (0.01s)
--- PASS: TestDNSProviders/example.com/99:_Create_TXTMulti_2 (0.01s)
--- PASS: TestDNSProviders/example.com/100:_Create_TXTMulti_3 (0.02s)
--- PASS: TestDNSProviders/example.com/101:_Create_TXTMulti_with_quotes (0.00s)
--- PASS: TestDNSProviders/example.com/102:_Change_TXTMulti (0.00s)
--- PASS: TestDNSProviders/example.com/103:_Empty (0.00s)
--- PASS: TestDNSProviders/example.com/104:_3x255-byte_TXTMulti (0.00s)
--- PASS: TestDNSProviders/example.com/105:_Empty (0.01s)
--- PASS: TestDNSProviders/example.com/106:_Create_some_records (0.00s)
--- PASS: TestDNSProviders/example.com/107:_Add_a_new_record_-_ignoring_foo (0.02s)
--- PASS: TestDNSProviders/example.com/108:_Empty (0.01s)
--- PASS: TestDNSProviders/example.com/109:_Create_some_records (0.01s)
--- PASS: TestDNSProviders/example.com/110:_Add_a_new_record_-_ignoring_*.foo (0.00s)
--- PASS: TestDNSProviders/example.com/111:_Empty (0.01s)

161118
list.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@ type DNSConfig struct {
Domains []*DomainConfig `json:"domains"` Domains []*DomainConfig `json:"domains"`
RegistrarsByName map[string]*RegistrarConfig `json:"-"` RegistrarsByName map[string]*RegistrarConfig `json:"-"`
DNSProvidersByName map[string]*DNSProviderConfig `json:"-"` DNSProvidersByName map[string]*DNSProviderConfig `json:"-"`
SkipRecordAudit bool `json:"skiprecordaudit,omitempty"`
} }
// FindDomain returns the *DomainConfig for domain query in config. // FindDomain returns the *DomainConfig for domain query in config.

View File

@ -108,7 +108,7 @@ type RecordConfig struct {
TlsaUsage uint8 `json:"tlsausage,omitempty"` TlsaUsage uint8 `json:"tlsausage,omitempty"`
TlsaSelector uint8 `json:"tlsaselector,omitempty"` TlsaSelector uint8 `json:"tlsaselector,omitempty"`
TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"` TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"`
TxtStrings []string `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores only the first one. TxtStrings []string `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores all the strings joined.
R53Alias map[string]string `json:"r53_alias,omitempty"` R53Alias map[string]string `json:"r53_alias,omitempty"`
AzureAlias map[string]string `json:"azure_alias,omitempty"` AzureAlias map[string]string `json:"azure_alias,omitempty"`
} }

View File

@ -1,45 +1,73 @@
package models package models
import ( /*
"fmt" Sadly many providers handle TXT records in strange and non-compliant ways.
"strings"
) The variations we've seen:
* a TXT record is a list of strings, each 255-octets or fewer.
* a TXT record is a list of strings, all but the last will be 255 octets in length.
* a TXT record is a string less than 256 octets.
* a TXT record is any length, but we'll split it into 255-octet chunks behind the scenes.
DNSControl stores the string as the user specified, then lets the
provider work out how to handle the given input. There are two
opportunties to work with the data:
1. The provider's AuditRecords() function is called to permit
the provider to return an error if it won't be able to handle the
contents. For example, it might detect that the string contains a char
the provider doesn't support (for example, a backtick). This auditing
is done without any communication to the provider's API. This allows
such errors to be detected at the "dnscontrol check" stage. 2. When
performing corrections (GetDomainCorrections()), the provider can slice
and dice the user's input however they want.
* If the user input is a list of strings:
* The strings are stored in RecordConfig.TxtStrings
* If the user input is a single string:
* The strings are stored in RecordConfig.TxtStrings[0]
In both cases, the .Target stores a string that can be used in error
messages and other UI messages. This string should not be used by the
provider in any other way because it has been modified from the user's
original input.
*/
// HasFormatIdenticalToTXT returns if a RecordConfig has a format which is // HasFormatIdenticalToTXT returns if a RecordConfig has a format which is
// identical to TXT, such as SPF. For more details, read // identical to TXT, such as SPF. For more details, read
// https://tools.ietf.org/html/rfc4408#section-3.1.1 // https://tools.ietf.org/html/rfc4408#section-3.1.1
func (rc *RecordConfig) HasFormatIdenticalToTXT() bool { func (rc *RecordConfig) HasFormatIdenticalToTXT() bool {
switch rc.Type { return rc.Type == "TXT" || rc.Type == "SPF"
case "SPF", "TXT":
return true
default:
return false
}
} }
// SetTargetTXT sets the TXT fields when there is 1 string. // SetTargetTXT sets the TXT fields when there is 1 string.
// The string is stored in .Target, and split into 255-octet chunks
// for .TxtStrings.
func (rc *RecordConfig) SetTargetTXT(s string) error { func (rc *RecordConfig) SetTargetTXT(s string) error {
rc.SetTarget(s)
rc.TxtStrings = []string{s}
if rc.Type == "" { if rc.Type == "" {
rc.Type = "TXT" rc.Type = "TXT"
} else if !rc.HasFormatIdenticalToTXT() {
panic("assertion failed: SetTargetTXT called when .Type is not TXT or compatible type")
} }
if !rc.HasFormatIdenticalToTXT() {
panic("assertion failed: SetTargetTXT called when .Type is not identical to TXT") rc.TxtStrings = []string{s}
} rc.SetTarget(rc.zoneFileQuoted())
return nil return nil
} }
// SetTargetTXTs sets the TXT fields when there are many strings. // SetTargetTXTs sets the TXT fields when there are many strings.
// The individual strings are stored in .TxtStrings, and joined to make .Target.
func (rc *RecordConfig) SetTargetTXTs(s []string) error { func (rc *RecordConfig) SetTargetTXTs(s []string) error {
rc.SetTarget(s[0])
rc.TxtStrings = s
if rc.Type == "" { if rc.Type == "" {
rc.Type = "TXT" rc.Type = "TXT"
} else if !rc.HasFormatIdenticalToTXT() {
panic("assertion failed: SetTargetTXTs called when .Type is not TXT or compatible type")
} }
if !rc.HasFormatIdenticalToTXT() {
panic("assertion failed: SetTargetTXT called when .Type is not identical to TXT") rc.TxtStrings = s
} rc.SetTarget(rc.zoneFileQuoted())
return nil return nil
} }
@ -51,43 +79,3 @@ func (rc *RecordConfig) SetTargetTXTs(s []string) error {
func (rc *RecordConfig) SetTargetTXTString(s string) error { func (rc *RecordConfig) SetTargetTXTString(s string) error {
return rc.SetTargetTXTs(ParseQuotedTxt(s)) return rc.SetTargetTXTs(ParseQuotedTxt(s))
} }
// TxtNormalize splits long txt targets if required based on the algo.
func (rc *RecordConfig) TxtNormalize(algo string) {
switch algo {
case "multistring":
rc.SetTargetTXTs(splitChunks(strings.Join(rc.TxtStrings, ""), 255))
case "space":
panic("not implemented")
default:
panic("TxtNormalize called with invalid algorithm")
}
}
func splitChunks(buf string, lim int) []string {
var chunk string
chunks := make([]string, 0, len(buf)/lim+1)
for len(buf) >= lim {
chunk, buf = buf[:lim], buf[lim:]
chunks = append(chunks, chunk)
}
if len(buf) > 0 {
chunks = append(chunks, buf[:])
}
return chunks
}
// ValidateTXT returns an error if the txt record is invalid.
// Verifies the Target and TxtStrings are less than 255 bytes each.
func ValidateTXT(rc *RecordConfig) error {
if !rc.HasFormatIdenticalToTXT() {
return fmt.Errorf("rc.Type=%q, expecting something identical to TXT", rc.Type)
}
for i := range rc.TxtStrings {
l := len(rc.TxtStrings[i])
if l > 255 {
return fmt.Errorf("txt target >255 bytes and AUTOSPLIT not set: label=%q index=%d len=%d string[:50]=%q", rc.GetLabel(), i, l, rc.TxtStrings[i][:50]+"...")
}
}
return nil
}

View File

@ -1,104 +0,0 @@
package models
import (
"reflect"
"strings"
"testing"
)
func TestValidateTXT_single(t *testing.T) {
tests := []struct {
d1 string
e1 bool
}{
{`x`, true},
{`foo`, true},
{strings.Repeat(`A`, 253), true},
{strings.Repeat(`B`, 254), true},
{strings.Repeat(`C`, 255), true},
{strings.Repeat(`D`, 256), false},
{strings.Repeat(`E`, 257), false},
}
for i, test := range tests {
rc := &RecordConfig{Type: "TXT"}
rc.SetTargetTXT(test.d1)
r := ValidateTXT(rc)
if test.e1 != (r == nil) {
s := "<nil>"
if r == nil {
s = "error"
}
t.Errorf("%v: expected (%v) got (%v) (%q)", i, s, r, test.d1)
}
}
}
func TestValidateTXT_multi(t *testing.T) {
s1 := "A"
s253 := strings.Repeat("C", 253)
s254 := strings.Repeat("C", 254)
s255 := strings.Repeat("D", 255)
s256 := strings.Repeat("E", 256)
s257 := strings.Repeat("E", 256)
tests := []struct {
d1 []string
e1 bool
}{
{[]string{`x`}, true},
{[]string{`foo`}, true},
{[]string{s253}, true},
{[]string{s254}, true},
{[]string{s255}, true},
{[]string{s256}, false},
{[]string{s257}, false},
{[]string{s1, s254}, true},
{[]string{s1, s255}, true},
{[]string{s1, s256}, false},
{[]string{s1, s254, s1}, true},
{[]string{s1, s255, s1}, true},
{[]string{s1, s256, s1}, false},
}
for i, test := range tests {
rc := &RecordConfig{Type: "TXT"}
rc.SetTargetTXTs(test.d1)
r := ValidateTXT(rc)
if test.e1 != (r == nil) {
s := "<nil>"
if r == nil {
s = "error"
}
t.Errorf("%v: expected (%v) got: (%v)", i, s, r)
}
}
}
func Test_splitChunks(t *testing.T) {
type args struct {
buf string
lim int
}
tests := []struct {
name string
args args
want []string
}{
{"0", args{"", 3}, []string{}},
{"1", args{"a", 3}, []string{"a"}},
{"2", args{"ab", 3}, []string{"ab"}},
{"3", args{"abc", 3}, []string{"abc"}},
{"4", args{"abcd", 3}, []string{"abc", "d"}},
{"5", args{"abcde", 3}, []string{"abc", "de"}},
{"6", args{"abcdef", 3}, []string{"abc", "def"}},
{"7", args{"abcdefg", 3}, []string{"abc", "def", "g"}},
{"8", args{"abcdefgh", 3}, []string{"abc", "def", "gh"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := splitChunks(tt.args.buf, tt.args.lim); !reflect.DeepEqual(got, tt.want) {
t.Errorf("splitChunks() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -23,9 +23,6 @@ so that it is easy to do things the right way in preparation.
// GetTargetField returns the target. There may be other fields (for example // GetTargetField returns the target. There may be other fields (for example
// an MX record also has a .MxPreference field. // an MX record also has a .MxPreference field.
func (rc *RecordConfig) GetTargetField() string { func (rc *RecordConfig) GetTargetField() string {
if rc.HasFormatIdenticalToTXT() {
return strings.Join(rc.TxtStrings, "")
}
return rc.target return rc.target
} }
@ -66,6 +63,11 @@ func (rc *RecordConfig) GetTargetCombined() string {
} }
} }
return rc.zoneFileQuoted()
}
// zoneFileQuoted returns the rData as would be quoted in a zonefile.
func (rc *RecordConfig) zoneFileQuoted() string {
// We cheat by converting to a dns.RR and use the String() function. // We cheat by converting to a dns.RR and use the String() function.
// This combines all the data for us, and even does proper quoting. // This combines all the data for us, and even does proper quoting.
// Sadly String() always includes a header, which we must strip out. // Sadly String() always includes a header, which we must strip out.

View File

@ -414,10 +414,8 @@ function isStringOrArray(x) {
} }
// AUTOSPLIT is a modifier that instructs the Go-level code to // AUTOSPLIT is deprecated. It is now a no-op.
// split this TXT record's target into chunks of 255. var AUTOSPLIT = { };
var AUTOSPLIT = { txtSplitAlgorithm: 'multistring' }; // Create 255-byte chunks
//var TXTMULTISPACE = { txtSplitAlgorithm: 'space' }; // Split on space [not implemented]
// TXT(name,target, recordModifiers...) // TXT(name,target, recordModifiers...)
var TXT = recordBuilder('TXT', { var TXT = recordBuilder('TXT', {
@ -427,20 +425,13 @@ var TXT = recordBuilder('TXT', {
], ],
transform: function(record, args, modifiers) { transform: function(record, args, modifiers) {
record.name = args.name; record.name = args.name;
// Store the strings twice: // Store the strings from the user verbatim.
// .target is the first string
// .txtstrings is the individual strings.
// NOTE: If there are more than 1 string, providers should only access
// .txtstrings, thus it doesn't matter what we store in .target.
// However, by storing the first string there, it improves backwards
// compatibility when the len(array) == 1 and (intentionally) breaks
// broken providers early in the integration tests.
if (_.isString(args.target)) { if (_.isString(args.target)) {
record.target = args.target;
record.txtstrings = [args.target]; record.txtstrings = [args.target];
record.target = args.target; // Overwritten by the Go code
} else { } else {
record.target = args.target[0];
record.txtstrings = args.target; record.txtstrings = args.target;
record.target = args.target.join(""); // Overwritten by the Go code
} }
}, },
}); });

View File

@ -212,135 +212,131 @@ var _escData = map[string]*_escFile{
"/helpers.js": { "/helpers.js": {
name: "helpers.js", name: "helpers.js",
local: "pkg/js/helpers.js", local: "pkg/js/helpers.js",
size: 28655, size: 28050,
modtime: 0, modtime: 0,
compressed: ` compressed: `
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
`, `,
}, },
} }

View File

@ -16,7 +16,7 @@ func TestCapabilitiesAreFiltered(t *testing.T) {
// Any capabilities which we wish to whitelist because it's not directly // Any capabilities which we wish to whitelist because it's not directly
// something we can test against. // something we can test against.
skipCheckCapabilities := make(map[string]struct{}) skipCheckCapabilities := make(map[string]struct{})
skipCheckCapabilities["CanUseTXTMulti"] = struct{}{} //skipCheckCapabilities["CanUseBlahBlahBlah"] = struct{}{}
fset := token.NewFileSet() fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, providersImportDir, nil, 0) pkgs, err := parser.ParseDir(fset, providersImportDir, nil, 0)

View File

@ -85,9 +85,6 @@ func validateRecordTypes(rec *models.RecordConfig, domain string, pTypes []strin
return nil return nil
} }
// these record types may contain underscores
var rTypeUnderscores = []string{"SRV", "TLSA", "TXT"}
func checkLabel(label string, rType string, target, domain string, meta map[string]string) error { func checkLabel(label string, rType string, target, domain string, meta map[string]string) error {
if label == "@" { if label == "@" {
return nil return nil
@ -108,7 +105,7 @@ func checkLabel(label string, rType string, target, domain string, meta map[stri
// are used in a way we consider typical. Yes, we're opinionated here. // are used in a way we consider typical. Yes, we're opinionated here.
// Don't warn for certain rtypes: // Don't warn for certain rtypes:
for _, ex := range rTypeUnderscores { for _, ex := range []string{"SRV", "TLSA", "TXT"} {
if rType == ex { if rType == ex {
return nil return nil
} }
@ -179,6 +176,17 @@ func checkTargets(rec *models.RecordConfig, domain string) (errs []error) {
return return
} }
// TODO: Write a test.
func checkTxtStrings(rc *models.RecordConfig) error {
for i := range rc.TxtStrings {
l := len([]byte(rc.TxtStrings[i]))
if l > 255 {
return fmt.Errorf("length of TxtStrings[%d] is %d, which is >255", i, l)
}
}
return nil
}
func transformCNAME(target, oldDomain, newDomain string) string { func transformCNAME(target, oldDomain, newDomain string) string {
// Canonicalize. If it isn't a FQDN, add the newDomain. // Canonicalize. If it isn't a FQDN, add the newDomain.
result := dnsutil.AddOrigin(target, oldDomain) result := dnsutil.AddOrigin(target, oldDomain)
@ -319,6 +327,11 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
if errs2 := checkTargets(rec, domain.Name); errs2 != nil { if errs2 := checkTargets(rec, domain.Name); errs2 != nil {
errs = append(errs, errs2...) errs = append(errs, errs2...)
} }
if rec.HasFormatIdenticalToTXT() { // i.e. if it is a TXT or SPF record.
if err := checkTxtStrings(rec); err != nil {
errs = append(errs, err)
}
}
// Canonicalize Targets. // Canonicalize Targets.
if rec.Type == "CNAME" || rec.Type == "MX" || rec.Type == "NAPTR" || rec.Type == "NS" || rec.Type == "SRV" { if rec.Type == "CNAME" || rec.Type == "MX" || rec.Type == "NAPTR" || rec.Type == "NS" || rec.Type == "SRV" {
@ -369,44 +382,6 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
errs = append(errs, ers...) errs = append(errs, ers...)
} }
// Split TXT targets that are >255 bytes (if permitted)
for _, domain := range config.Domains {
for _, rec := range domain.Records {
if rec.HasFormatIdenticalToTXT() {
if txtAlgo, ok := rec.Metadata["txtSplitAlgorithm"]; ok {
rec.TxtNormalize(txtAlgo)
}
}
}
}
// Validate TXT records.
for _, domain := range config.Domains {
// Collect the names of providers that don't support TXTMulti:
txtMultiDissenters := []string{}
for _, provider := range domain.DNSProviderInstances {
pType := provider.ProviderType
if !providers.ProviderHasCapability(pType, providers.CanUseTXTMulti) {
txtMultiDissenters = append(txtMultiDissenters, provider.Name)
}
}
// Validate TXT records.
for _, rec := range domain.Records {
if rec.HasFormatIdenticalToTXT() {
// If TXTMulti is required, all providers must support that feature.
if len(rec.TxtStrings) > 1 && len(txtMultiDissenters) > 0 {
errs = append(errs,
fmt.Errorf("%s records with multiple strings not supported by %s (label=%q domain=%v)",
rec.Type, strings.Join(txtMultiDissenters, ","), rec.GetLabel(), domain.Name))
}
// Validate the record:
if err := models.ValidateTXT(rec); err != nil {
errs = append(errs, err)
}
}
}
}
// Process IMPORT_TRANSFORM // Process IMPORT_TRANSFORM
for _, domain := range config.Domains { for _, domain := range config.Domains {
for _, rec := range domain.Records { for _, rec := range domain.Records {
@ -459,6 +434,17 @@ func ValidateAndNormalizeConfig(config *models.DNSConfig) (errs []error) {
errs = append(errs, checkAutoDNSSEC(d)...) errs = append(errs, checkAutoDNSSEC(d)...)
} }
// At this point we've munged anything that needs to be munged, and
// validated anything that can be globally validated.
// Let's ask // the provider if there are any records they can't handle.
for _, domain := range config.Domains { // For each domain..
for _, provider := range domain.DNSProviderInstances { // For each provider...
if err := providers.AuditRecords(provider.ProviderBase.ProviderType, domain.Records); err != nil {
errs = append(errs, err)
}
}
}
return errs return errs
} }

View File

@ -293,14 +293,14 @@ const (
) )
func init() { func init() {
providers.RegisterDomainServiceProviderType(ProviderNoDS, nil, providers.DocumentationNotes{}) providers.RegisterDomainServiceProviderType(ProviderNoDS, providers.DspFuncs{}, providers.DocumentationNotes{})
providers.RegisterDomainServiceProviderType(ProviderFullDS, nil, providers.DocumentationNotes{ providers.RegisterDomainServiceProviderType(ProviderFullDS, providers.DspFuncs{}, providers.DocumentationNotes{
providers.CanUseDS: providers.Can(), providers.CanUseDS: providers.Can(),
}) })
providers.RegisterDomainServiceProviderType(ProviderChildDSOnly, nil, providers.DocumentationNotes{ providers.RegisterDomainServiceProviderType(ProviderChildDSOnly, providers.DspFuncs{}, providers.DocumentationNotes{
providers.CanUseDSForChildren: providers.Can(), providers.CanUseDSForChildren: providers.Can(),
}) })
providers.RegisterDomainServiceProviderType(ProviderBothDSCaps, nil, providers.DocumentationNotes{ providers.RegisterDomainServiceProviderType(ProviderBothDSCaps, providers.DspFuncs{}, providers.DocumentationNotes{
providers.CanUseDS: providers.Can(), providers.CanUseDS: providers.Can(),
providers.CanUseDSForChildren: providers.Can(), providers.CanUseDSForChildren: providers.Can(),
}) })

144
pkg/recordaudit/txt.go Normal file
View File

@ -0,0 +1,144 @@
package recordaudit
import (
"fmt"
"strings"
"github.com/StackExchange/dnscontrol/v3/models"
)
// Keep these in alphabetical order.
// TxtNoBackticks audits TXT records for strings that contain backticks.
func TxtNoBackticks(records []*models.RecordConfig) error {
for _, rc := range records {
if rc.HasFormatIdenticalToTXT() {
for _, txt := range rc.TxtStrings {
if strings.Index(txt, "`") != -1 {
return fmt.Errorf("txtstring contains backtick")
}
}
}
}
return nil
}
// TxtNoSingleQuotes audits TXT records for strings that contain single-quotes.
func TxtNoSingleQuotes(records []*models.RecordConfig) error {
for _, rc := range records {
if rc.HasFormatIdenticalToTXT() {
for _, txt := range rc.TxtStrings {
if strings.Index(txt, "'") != -1 {
return fmt.Errorf("txtstring contains single-quotes")
}
}
}
}
return nil
}
// TxtNoDoubleQuotes audits TXT records for strings that contain doublequotes.
func TxtNoDoubleQuotes(records []*models.RecordConfig) error {
for _, rc := range records {
if rc.HasFormatIdenticalToTXT() {
for _, txt := range rc.TxtStrings {
if strings.Index(txt, `"`) != -1 {
return fmt.Errorf("txtstring contains doublequotes")
}
}
}
}
return nil
}
// TxtNoLen255 audits TXT records for strings exactly 255 octets long.
func TxtNoLen255(records []*models.RecordConfig) error {
for _, rc := range records {
if rc.HasFormatIdenticalToTXT() { // TXT and similar:
for _, txt := range rc.TxtStrings {
if len(txt) == 255 {
return fmt.Errorf("txtstring length is 255")
}
}
}
}
return nil
}
// TxtNoLongStrings audits TXT records for strings that are >255 octets.
func TxtNoLongStrings(records []*models.RecordConfig) error {
for _, rc := range records {
if rc.HasFormatIdenticalToTXT() { // TXT and similar:
for _, txt := range rc.TxtStrings {
if len(txt) > 255 {
return fmt.Errorf("txtstring length > 255")
}
}
}
}
return nil
}
// TxtNoMultipleStrings audits TXT records for multiple strings
func TxtNoMultipleStrings(records []*models.RecordConfig) error {
for _, rc := range records {
if rc.HasFormatIdenticalToTXT() { // TXT and similar:
if len(rc.TxtStrings) > 1 {
return fmt.Errorf("multiple strings in one txt")
} else if len(rc.TxtStrings) == 1 && len(rc.TxtStrings[0]) > 255 {
return fmt.Errorf("strings >255 octets")
}
}
}
return nil
}
// TxtNoTrailingSpace audits TXT records for strings that end with space.
func TxtNoTrailingSpace(records []*models.RecordConfig) error {
for _, rc := range records {
if rc.HasFormatIdenticalToTXT() { // TXT and similar:
for _, txt := range rc.TxtStrings {
if txt != "" && txt[ultimate(txt)] == ' ' {
return fmt.Errorf("txtstring ends with space")
}
}
}
}
return nil
}
// TxtNotEmpty audits TXT records for empty strings.
func TxtNotEmpty(records []*models.RecordConfig) error {
for _, rc := range records {
if rc.HasFormatIdenticalToTXT() { // TXT and similar:
// There must be strings.
if len(rc.TxtStrings) == 0 {
return fmt.Errorf("txt with no strings")
}
// Each string must be non-empty.
for _, txt := range rc.TxtStrings {
if len(txt) == 0 {
return fmt.Errorf("txtstring is empty")
}
}
}
}
return nil
}

View File

@ -0,0 +1,21 @@
package recordaudit
/*
I proposed that Go add something like "len()" that returns the highest
index. This would avoid off-by-one errors. The proposed names include
ultimate(), ult(), high(), highest().
Nay-sayers said I should implement this as a function and see if I
actually used it. (I suspect the nay-sayers are perfect people that
never make off-by-one errors.)
That's what this file is about. It should be exactly the same (except
the first line) anywhere this is needed. After a few years I'll be
able to report if it actually helped.
Go will in-line this function.
*/
func ultimate(s string) int {
return len(s) - 1
}

33
pkg/txtutil/txtutil.go Normal file
View File

@ -0,0 +1,33 @@
package txtutil
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// SplitSingleLongTxt finds TXT records with a single long string and splits it
// into 255-octet chunks. This is used by providers that, when a user specifies
// one long TXT string, split it into smaller strings behind the scenes.
// Typically this replaces the TXTMulti capability.
func SplitSingleLongTxt(records []*models.RecordConfig) {
for _, rc := range records {
if rc.HasFormatIdenticalToTXT() {
s := rc.TxtStrings[0]
if len(rc.TxtStrings) == 1 && len(s) > 255 {
rc.SetTargetTXTs(splitChunks(s, 255))
}
}
}
}
func splitChunks(buf string, lim int) []string {
var chunk string
chunks := make([]string, 0, len(buf)/lim+1)
for len(buf) >= lim {
chunk, buf = buf[:lim], buf[lim:]
chunks = append(chunks, chunk)
}
if len(buf) > 0 {
chunks = append(chunks, buf[:])
}
return chunks
}

View File

@ -0,0 +1,35 @@
package txtutil
import (
"reflect"
"testing"
)
func Test_splitChunks(t *testing.T) {
type args struct {
buf string
lim int
}
tests := []struct {
name string
args args
want []string
}{
{"0", args{"", 3}, []string{}},
{"1", args{"a", 3}, []string{"a"}},
{"2", args{"ab", 3}, []string{"ab"}},
{"3", args{"abc", 3}, []string{"abc"}},
{"4", args{"abcd", 3}, []string{"abc", "d"}},
{"5", args{"abcde", 3}, []string{"abc", "de"}},
{"6", args{"abcdef", 3}, []string{"abc", "def"}},
{"7", args{"abcdefg", 3}, []string{"abc", "def", "g"}},
{"8", args{"abcdefgh", 3}, []string{"abc", "def", "gh"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := splitChunks(tt.args.buf, tt.args.lim); !reflect.DeepEqual(got, tt.want) {
t.Errorf("splitChunks() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -30,7 +30,11 @@ var features = providers.DocumentationNotes{
// Register with the dnscontrol system. // Register with the dnscontrol system.
// This establishes the name (all caps), and the function to call to initialize it. // This establishes the name (all caps), and the function to call to initialize it.
func init() { func init() {
providers.RegisterDomainServiceProviderType("ACTIVEDIRECTORY_PS", newDNS, features) fns := providers.DspFuncs{
Initializer: newDNS,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("ACTIVEDIRECTORY_PS", fns, features)
} }
func newDNS(config map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { func newDNS(config map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {

View File

@ -0,0 +1,11 @@
package activedir
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -0,0 +1,11 @@
package axfrddns
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -25,6 +25,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
) )
@ -41,7 +42,6 @@ var features = providers.DocumentationNotes{
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(), providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(), providers.CanUseTLSA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.CanAutoDNSSEC: providers.Can("Just warn when DNSSEC is requested but no RRSIG is found in the AXFR or warn when DNSSEC is not requested but RRSIG are found in the AXFR."), providers.CanAutoDNSSEC: providers.Can("Just warn when DNSSEC is requested but no RRSIG is found in the AXFR or warn when DNSSEC is not requested but RRSIG are found in the AXFR."),
providers.CantUseNOPURGE: providers.Cannot(), providers.CantUseNOPURGE: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot(), providers.DocCreateDomains: providers.Cannot(),
@ -117,7 +117,11 @@ func initAxfrDdns(config map[string]string, providermeta json.RawMessage) (provi
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("AXFRDDNS", initAxfrDdns, features) fns := providers.DspFuncs{
Initializer: initAxfrDdns,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("AXFRDDNS", fns, features)
} }
// Param is used to decode extra parameters sent to provider. // Param is used to decode extra parameters sent to provider.
@ -289,6 +293,7 @@ func (c *axfrddnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
// Normalize // Normalize
models.PostProcessRecords(foundRecords) models.PostProcessRecords(foundRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc) differ := diff.New(dc)
_, create, del, mod, err := differ.IncrementalDiff(foundRecords) _, create, del, mod, err := differ.IncrementalDiff(foundRecords)

View File

@ -0,0 +1,11 @@
package azuredns
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -14,6 +14,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
) )
@ -58,7 +59,6 @@ var features = providers.DocumentationNotes{
providers.DocOfficiallySupported: providers.Can(), providers.DocOfficiallySupported: providers.Can(),
providers.CanUsePTR: providers.Can(), providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.CanUseCAA: providers.Can(), providers.CanUseCAA: providers.Can(),
providers.CanUseNAPTR: providers.Cannot(), providers.CanUseNAPTR: providers.Cannot(),
providers.CanUseSSHFP: providers.Cannot(), providers.CanUseSSHFP: providers.Cannot(),
@ -68,7 +68,11 @@ var features = providers.DocumentationNotes{
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("AZURE_DNS", newAzureDNSDsp, features) fns := providers.DspFuncs{
Initializer: newAzureDNSDsp,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("AZURE_DNS", fns, features)
providers.RegisterCustomRecordType("AZURE_ALIAS", "AZURE_DNS", "") providers.RegisterCustomRecordType("AZURE_ALIAS", "AZURE_DNS", "")
} }
@ -182,7 +186,10 @@ func (a *azurednsProvider) getExistingRecords(domain string) (models.Records, []
existingRecords = append(existingRecords, nativeToRecords(set, zoneName)...) existingRecords = append(existingRecords, nativeToRecords(set, zoneName)...)
} }
// FIXME(tlim): PostProcessRecords is usually called in GetDomainCorrections.
models.PostProcessRecords(existingRecords) models.PostProcessRecords(existingRecords)
// FIXME(tlim): The "records" return value is usually stored in RecordConfig.Original.
return existingRecords, records, zoneName, nil return existingRecords, records, zoneName, nil
} }
@ -200,6 +207,8 @@ func (a *azurednsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
return nil, err return nil, err
} }
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc) differ := diff.New(dc)
namesToUpdate, err := differ.ChangedGroups(existingRecords) namesToUpdate, err := differ.ChangedGroups(existingRecords)
if err != nil { if err != nil {

View File

@ -0,0 +1,11 @@
package bind
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -28,6 +28,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/prettyzone" "github.com/StackExchange/dnscontrol/v3/pkg/prettyzone"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
) )
@ -39,7 +40,6 @@ var features = providers.DocumentationNotes{
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(), providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(), providers.CanUseTLSA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.CanAutoDNSSEC: providers.Can("Just writes out a comment indicating DNSSEC was requested"), providers.CanAutoDNSSEC: providers.Can("Just writes out a comment indicating DNSSEC was requested"),
providers.CantUseNOPURGE: providers.Cannot(), providers.CantUseNOPURGE: providers.Cannot(),
providers.DocCreateDomains: providers.Can("Driver just maintains list of zone files. It should automatically add missing ones."), providers.DocCreateDomains: providers.Can("Driver just maintains list of zone files. It should automatically add missing ones."),
@ -77,7 +77,11 @@ func initBind(config map[string]string, providermeta json.RawMessage) (providers
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("BIND", initBind, features) fns := providers.DspFuncs{
Initializer: initBind,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("BIND", fns, features)
} }
// SoaInfo contains the parts of the default SOA settings. // SoaInfo contains the parts of the default SOA settings.
@ -228,6 +232,7 @@ func (c *bindProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.
// Normalize // Normalize
models.PostProcessRecords(foundRecords) models.PostProcessRecords(foundRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc) differ := diff.New(dc)
_, create, del, mod, err := differ.IncrementalDiff(foundRecords) _, create, del, mod, err := differ.IncrementalDiff(foundRecords)

View File

@ -41,9 +41,6 @@ const (
// CanUseTLSA indicates the provider can handle TLSA records // CanUseTLSA indicates the provider can handle TLSA records
CanUseTLSA CanUseTLSA
// CanUseTXTMulti indicates the provider can handle TXT records with multiple strings
CanUseTXTMulti
// CanAutoDNSSEC indicates that the provider can automatically handle DNSSEC, // CanAutoDNSSEC indicates that the provider can automatically handle DNSSEC,
// so folks can ask for that. // so folks can ask for that.
CanAutoDNSSEC CanAutoDNSSEC

View File

@ -17,20 +17,19 @@ func _() {
_ = x[CanUseSRV-6] _ = x[CanUseSRV-6]
_ = x[CanUseSSHFP-7] _ = x[CanUseSSHFP-7]
_ = x[CanUseTLSA-8] _ = x[CanUseTLSA-8]
_ = x[CanUseTXTMulti-9] _ = x[CanAutoDNSSEC-9]
_ = x[CanAutoDNSSEC-10] _ = x[CantUseNOPURGE-10]
_ = x[CantUseNOPURGE-11] _ = x[DocOfficiallySupported-11]
_ = x[DocOfficiallySupported-12] _ = x[DocDualHost-12]
_ = x[DocDualHost-13] _ = x[DocCreateDomains-13]
_ = x[DocCreateDomains-14] _ = x[CanUseRoute53Alias-14]
_ = x[CanUseRoute53Alias-15] _ = x[CanGetZones-15]
_ = x[CanGetZones-16] _ = x[CanUseAzureAlias-16]
_ = x[CanUseAzureAlias-17]
} }
const _Capability_name = "CanUseAliasCanUseCAACanUseDSCanUseDSForChildrenCanUsePTRCanUseNAPTRCanUseSRVCanUseSSHFPCanUseTLSACanUseTXTMultiCanAutoDNSSECCantUseNOPURGEDocOfficiallySupportedDocDualHostDocCreateDomainsCanUseRoute53AliasCanGetZonesCanUseAzureAlias" const _Capability_name = "CanUseAliasCanUseCAACanUseDSCanUseDSForChildrenCanUsePTRCanUseNAPTRCanUseSRVCanUseSSHFPCanUseTLSACanAutoDNSSECCantUseNOPURGEDocOfficiallySupportedDocDualHostDocCreateDomainsCanUseRoute53AliasCanGetZonesCanUseAzureAlias"
var _Capability_index = [...]uint8{0, 11, 20, 28, 47, 56, 67, 76, 87, 97, 111, 124, 138, 160, 171, 187, 205, 216, 232} var _Capability_index = [...]uint8{0, 11, 20, 28, 47, 56, 67, 76, 87, 97, 110, 124, 146, 157, 173, 191, 202, 218}
func (i Capability) String() string { func (i Capability) String() string {
if i >= Capability(len(_Capability_index)-1) { if i >= Capability(len(_Capability_index)-1) {

View File

@ -0,0 +1,11 @@
package cloudflare
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -14,6 +14,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/printer" "github.com/StackExchange/dnscontrol/v3/pkg/printer"
"github.com/StackExchange/dnscontrol/v3/pkg/transform" "github.com/StackExchange/dnscontrol/v3/pkg/transform"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
) )
@ -45,7 +46,6 @@ var features = providers.DocumentationNotes{
providers.CanUseTLSA: providers.Can(), providers.CanUseTLSA: providers.Can(),
providers.CanUseSSHFP: providers.Can(), providers.CanUseSSHFP: providers.Can(),
providers.CanUseDSForChildren: providers.Can(), providers.CanUseDSForChildren: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.DocCreateDomains: providers.Can(), providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Cannot("Cloudflare will not work well in situations where it is not the only DNS server"), providers.DocDualHost: providers.Cannot("Cloudflare will not work well in situations where it is not the only DNS server"),
providers.DocOfficiallySupported: providers.Can(), providers.DocOfficiallySupported: providers.Can(),
@ -53,7 +53,11 @@ var features = providers.DocumentationNotes{
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", newCloudflare, features) fns := providers.DspFuncs{
Initializer: newCloudflare,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", fns, features)
providers.RegisterCustomRecordType("CF_REDIRECT", "CLOUDFLAREAPI", "") providers.RegisterCustomRecordType("CF_REDIRECT", "CLOUDFLAREAPI", "")
providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", "CLOUDFLAREAPI", "") providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", "CLOUDFLAREAPI", "")
} }
@ -206,6 +210,7 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
// Normalize // Normalize
models.PostProcessRecords(records) models.PostProcessRecords(records)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc, getProxyMetadata) differ := diff.New(dc, getProxyMetadata)
_, create, del, mod, err := differ.IncrementalDiff(records) _, create, del, mod, err := differ.IncrementalDiff(records)

View File

@ -195,9 +195,7 @@ func (c *cloudflareProvider) createRec(rec *models.RecordConfig, domainID string
prio = fmt.Sprintf(" %d ", rec.MxPreference) prio = fmt.Sprintf(" %d ", rec.MxPreference)
} }
if rec.Type == "TXT" { if rec.Type == "TXT" {
if len(rec.TxtStrings) > 1 { content = rec.GetTargetField()
content = `"` + strings.Join(rec.TxtStrings, `" "`) + `"`
}
} }
if rec.Type == "DS" { if rec.Type == "DS" {
content = fmt.Sprintf("%d %d %d %s", rec.DsKeyTag, rec.DsAlgorithm, rec.DsDigestType, rec.DsDigest) content = fmt.Sprintf("%d %d %d %s", rec.DsKeyTag, rec.DsAlgorithm, rec.DsDigestType, rec.DsDigest)

View File

@ -0,0 +1,28 @@
package cloudns
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.TxtNoBackticks(records); err != nil {
return err
}
// Still needed as of 2021-03-01
if err := recordaudit.TxtNotEmpty(records); err != nil {
return err
}
// Still needed as of 2021-03-01
if err := recordaudit.TxtNoTrailingSpace(records); err != nil {
return err
}
// Still needed as of 2021-03-01
return nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
) )
@ -49,12 +50,15 @@ var features = providers.DocumentationNotes{
providers.CanUsePTR: providers.Can(), providers.CanUsePTR: providers.Can(),
providers.CanGetZones: providers.Can(), providers.CanGetZones: providers.Can(),
providers.CanUseDSForChildren: providers.Can(), providers.CanUseDSForChildren: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
//providers.CanUseDS: providers.Can(), // in ClouDNS we can add DS record just for a subdomain(child) //providers.CanUseDS: providers.Can(), // in ClouDNS we can add DS record just for a subdomain(child)
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("CLOUDNS", NewCloudns, features) fns := providers.DspFuncs{
Initializer: NewCloudns,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("CLOUDNS", fns, features)
} }
// GetNameservers returns the nameservers for a domain. // GetNameservers returns the nameservers for a domain.
@ -90,6 +94,7 @@ func (c *cloudnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
} }
// Normalize // Normalize
models.PostProcessRecords(existingRecords) models.PostProcessRecords(existingRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
// ClouDNS doesn't allow selecting an arbitrary TTL, only a set of predefined values https://asia.cloudns.net/wiki/article/188/ // ClouDNS doesn't allow selecting an arbitrary TTL, only a set of predefined values https://asia.cloudns.net/wiki/article/188/
// We need to make sure we don't change it every time if it is as close as it's going to get // We need to make sure we don't change it every time if it is as close as it's going to get

View File

@ -0,0 +1,11 @@
package cscglobal
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -0,0 +1,11 @@
package desec
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -27,7 +27,6 @@ func nativeToRecords(n resourceRecord, origin string) (rcs []*models.RecordConfi
switch rtype := n.Type; rtype { switch rtype := n.Type; rtype {
case "TXT": case "TXT":
rc.SetTargetTXT(value) rc.SetTargetTXT(value)
rc.TxtNormalize("multistring")
default: // "A", "AAAA", "CAA", "NS", "CNAME", "MX", "PTR", "SRV" default: // "A", "AAAA", "CAA", "NS", "CNAME", "MX", "PTR", "SRV"
if err := rc.PopulateFromString(rtype, value, origin); err != nil { if err := rc.PopulateFromString(rtype, value, origin); err != nil {
panic(fmt.Errorf("unparsable record received from deSEC: %w", err)) panic(fmt.Errorf("unparsable record received from deSEC: %w", err))

View File

@ -48,7 +48,6 @@ var features = providers.DocumentationNotes{
providers.CanUsePTR: providers.Can(), providers.CanUsePTR: providers.Can(),
providers.CanGetZones: providers.Can(), providers.CanGetZones: providers.Can(),
providers.CanAutoDNSSEC: providers.Cannot(), providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanUseTXTMulti: providers.Can(),
} }
var defaultNameServerNames = []string{ var defaultNameServerNames = []string{
@ -57,7 +56,11 @@ var defaultNameServerNames = []string{
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("DESEC", NewDeSec, features) fns := providers.DspFuncs{
Initializer: NewDeSec,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("DESEC", fns, features)
} }
// GetNameservers returns the nameservers for a domain. // GetNameservers returns the nameservers for a domain.

View File

@ -0,0 +1,61 @@
package digitalocean
import (
"fmt"
"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 {
// TODO(tlim): Audit CAA records.
// "Semicolons not supported in issue/issuewild fields.", "https://www.digitalocean.com/docs/networking/dns/how-to/create-caa-records"),
// Users are warned about these limits in docs/_providers/digitalocean.md
if err := MaxLengthDO(records); err != nil {
return err
}
// Still needed as of 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.
if err := recordaudit.TxtNoDoubleQuotes(records); err != nil {
return err
}
// Still needed as of 2021-03-01
return nil
}
// MaxLengthDO returns and error if the strings are longer than
// permitted by DigitalOcean. Sadly their length limit is
// undocumented. This is a guess.
func MaxLengthDO(records []*models.RecordConfig) error {
// The total length of all strings can't be longer than 512; and in
// reality must be shorter due to sloppy validation checks.
// https://github.com/StackExchange/dnscontrol/issues/370
// DigitalOcean's TXT record implementation checks size limits
// wrong. RFC 1035 Section 3.3.14 states that each substring can be
// 255 octets, and there is no limit on the number of such
// substrings, aside from the usual packet length limits. DO's
// implementation restricts the total length to be 512 octets,
// including the quotes, backlashes used for escapes, spaces between
// substrings.
// In other words, they're doing the checking on the API protocol
// encoded data instead of on on the resulting TXT record. Sigh.
for _, rc := range records {
if rc.HasFormatIdenticalToTXT() { // TXT and similar:
if len(rc.GetTargetField()) > 509 {
return fmt.Errorf("encoded txt too long")
}
}
}
return nil
}

View File

@ -10,6 +10,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
"github.com/miekg/dns/dnsutil" "github.com/miekg/dns/dnsutil"
@ -76,11 +77,14 @@ var features = providers.DocumentationNotes{
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseCAA: providers.Can("Semicolons not supported in issue/issuewild fields.", "https://www.digitalocean.com/docs/networking/dns/how-to/create-caa-records"), providers.CanUseCAA: providers.Can("Semicolons not supported in issue/issuewild fields.", "https://www.digitalocean.com/docs/networking/dns/how-to/create-caa-records"),
providers.CanGetZones: providers.Can(), providers.CanGetZones: providers.Can(),
providers.CanUseTXTMulti: providers.Can("A broken parser prevents TXTMulti strings from including double-quotes; The total length of all strings can't be longer than 512; and in reality must be shorter due to sloppy validation checks.", "https://github.com/StackExchange/dnscontrol/issues/370"),
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("DIGITALOCEAN", NewDo, features) fns := providers.DspFuncs{
Initializer: NewDo,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("DIGITALOCEAN", fns, features)
} }
// EnsureDomainExists returns an error if domain doesn't exist. // EnsureDomainExists returns an error if domain doesn't exist.
@ -140,6 +144,7 @@ func (api *digitaloceanProvider) GetDomainCorrections(dc *models.DomainConfig) (
// Normalize // Normalize
models.PostProcessRecords(existingRecords) models.PostProcessRecords(existingRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc) differ := diff.New(dc)
_, create, delete, modify, err := differ.IncrementalDiff(existingRecords) _, create, delete, modify, err := differ.IncrementalDiff(existingRecords)
@ -290,11 +295,11 @@ func toReq(dc *models.DomainConfig, rc *models.RecordConfig) *godo.DomainRecordE
priority = int(rc.SrvPriority) priority = int(rc.SrvPriority)
case "TXT": case "TXT":
// TXT records are the one place where DO combines many items into one field. // TXT records are the one place where DO combines many items into one field.
target = rc.GetTargetCombined() target = rc.GetTargetField()
case "CAA": case "CAA":
// DO API requires that value ends in dot // DO API requires that a CAA target ends in dot.
// But the value returned from API doesn't contain this, // Interestingly enough, the value returned from API doesn't
// so no need to strip the dot when reading value from API. // contain a trailing dot.
target = target + "." target = target + "."
default: default:
// no action required // no action required

View File

@ -0,0 +1,11 @@
package dnsimple
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -13,6 +13,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
) )
@ -24,7 +25,6 @@ var features = providers.DocumentationNotes{
providers.CanUsePTR: providers.Can(), providers.CanUsePTR: providers.Can(),
providers.CanUseSSHFP: providers.Can(), providers.CanUseSSHFP: providers.Can(),
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.CanAutoDNSSEC: providers.Can(), providers.CanAutoDNSSEC: providers.Can(),
providers.CanUseTLSA: providers.Cannot(), providers.CanUseTLSA: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot(), providers.DocCreateDomains: providers.Cannot(),
@ -35,7 +35,11 @@ var features = providers.DocumentationNotes{
func init() { func init() {
providers.RegisterRegistrarType("DNSIMPLE", newReg) providers.RegisterRegistrarType("DNSIMPLE", newReg)
providers.RegisterDomainServiceProviderType("DNSIMPLE", newDsp, features) fns := providers.DspFuncs{
Initializer: newDsp,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("DNSIMPLE", fns, features)
} }
const stateRegistered = "registered" const stateRegistered = "registered"
@ -145,6 +149,7 @@ func (c *dnsimpleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
// Normalize // Normalize
models.PostProcessRecords(actual) models.PostProcessRecords(actual)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc) differ := diff.New(dc)
_, create, del, modify, err := differ.IncrementalDiff(actual) _, create, del, modify, err := differ.IncrementalDiff(actual)

View File

@ -0,0 +1,11 @@
package doh
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -0,0 +1,11 @@
package exoscale
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -37,7 +37,11 @@ var features = providers.DocumentationNotes{
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("EXOSCALE", NewExoscale, features) fns := providers.DspFuncs{
Initializer: NewExoscale,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("EXOSCALE", fns, features)
} }
// EnsureDomainExists returns an error if domain doesn't exist. // EnsureDomainExists returns an error if domain doesn't exist.

View File

@ -0,0 +1,11 @@
package gandi5
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -27,6 +27,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/printer" "github.com/StackExchange/dnscontrol/v3/pkg/printer"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
) )
@ -34,7 +35,11 @@ import (
// init registers the provider to dnscontrol. // init registers the provider to dnscontrol.
func init() { func init() {
providers.RegisterDomainServiceProviderType("GANDI_V5", newDsp, features) fns := providers.DspFuncs{
Initializer: newDsp,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("GANDI_V5", fns, features)
providers.RegisterRegistrarType("GANDI_V5", newReg) providers.RegisterRegistrarType("GANDI_V5", newReg)
} }
@ -46,7 +51,6 @@ var features = providers.DocumentationNotes{
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(), providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(), providers.CanUseTLSA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.CantUseNOPURGE: providers.Cannot(), providers.CantUseNOPURGE: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot("Can only manage domains registered through their service"), providers.DocCreateDomains: providers.Cannot("Can only manage domains registered through their service"),
providers.DocOfficiallySupported: providers.Cannot(), providers.DocOfficiallySupported: providers.Cannot(),
@ -194,6 +198,8 @@ func (client *gandiv5Provider) GenerateDomainCorrections(dc *models.DomainConfig
var corrections = []*models.Correction{} var corrections = []*models.Correction{}
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
// diff existing vs. current. // diff existing vs. current.
differ := diff.New(dc) differ := diff.New(dc)
keysToUpdate, err := differ.ChangedGroups(existing) keysToUpdate, err := differ.ChangedGroups(existing)

View File

@ -0,0 +1,11 @@
package google
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -14,6 +14,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
gdns "google.golang.org/api/dns/v1" gdns "google.golang.org/api/dns/v1"
) )
@ -26,7 +27,6 @@ var features = providers.DocumentationNotes{
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(), providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(), providers.CanUseTLSA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.DocCreateDomains: providers.Can(), providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(), providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Can(), providers.DocOfficiallySupported: providers.Can(),
@ -37,7 +37,11 @@ func sPtr(s string) *string {
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("GCLOUD", New, features) fns := providers.DspFuncs{
Initializer: New,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("GCLOUD", fns, features)
} }
type gcloudProvider struct { type gcloudProvider struct {
@ -187,6 +191,7 @@ func (g *gcloudProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*model
// Normalize // Normalize
models.PostProcessRecords(existingRecords) models.PostProcessRecords(existingRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
// first collect keys that have changed // first collect keys that have changed
differ := diff.New(dc) differ := diff.New(dc)

View File

@ -0,0 +1,11 @@
package hedns
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -18,6 +18,7 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
) )
@ -51,7 +52,6 @@ var features = providers.DocumentationNotes{
providers.CanUseSSHFP: providers.Can(), providers.CanUseSSHFP: providers.Can(),
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseTLSA: providers.Cannot(), providers.CanUseTLSA: providers.Cannot(),
providers.CanUseTXTMulti: providers.Can(),
providers.CanAutoDNSSEC: providers.Cannot(), providers.CanAutoDNSSEC: providers.Cannot(),
providers.DocCreateDomains: providers.Can(), providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(), providers.DocDualHost: providers.Can(),
@ -60,7 +60,11 @@ var features = providers.DocumentationNotes{
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("HEDNS", newHEDNSProvider, features) fns := providers.DspFuncs{
Initializer: newHEDNSProvider,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("HEDNS", fns, features)
} }
var defaultNameservers = []string{ var defaultNameservers = []string{
@ -199,6 +203,7 @@ func (c *hednsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models
// Normalize // Normalize
models.PostProcessRecords(prunedRecords) models.PostProcessRecords(prunedRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc) differ := diff.New(dc)
_, toCreate, toDelete, toModify, err := differ.IncrementalDiff(prunedRecords) _, toCreate, toDelete, toModify, err := differ.IncrementalDiff(prunedRecords)

View File

@ -0,0 +1,11 @@
package hetzner
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -7,6 +7,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
) )
@ -22,11 +23,14 @@ var features = providers.DocumentationNotes{
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Cannot(), providers.CanUseSSHFP: providers.Cannot(),
providers.CanUseTLSA: providers.Cannot(), providers.CanUseTLSA: providers.Cannot(),
providers.CanUseTXTMulti: providers.Can(),
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("HETZNER", New, features) fns := providers.DspFuncs{
Initializer: New,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("HETZNER", fns, features)
} }
// New creates a new API handle. // New creates a new API handle.
@ -93,6 +97,7 @@ func (api *hetznerProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mo
// Normalize // Normalize
models.PostProcessRecords(existingRecords) models.PostProcessRecords(existingRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc) differ := diff.New(dc)
_, create, del, modify, err := differ.IncrementalDiff(existingRecords) _, create, del, modify, err := differ.IncrementalDiff(existingRecords)

View File

@ -2,6 +2,7 @@ package hetzner
import ( import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"strings"
) )
type bulkCreateRecordsRequest struct { type bulkCreateRecordsRequest struct {
@ -63,20 +64,22 @@ func fromRecordConfig(in *models.RecordConfig, zone *zone) *record {
record := &record{ record := &record{
Name: in.GetLabel(), Name: in.GetLabel(),
Type: in.Type, Type: in.Type,
Value: in.GetTargetField(), Value: in.GetTargetCombined(),
TTL: &ttl, TTL: &ttl,
ZoneID: zone.ID, ZoneID: zone.ID,
} }
switch record.Type { if record.Type == "TXT" && len(in.TxtStrings) == 1 {
case "TXT": // HACK: HETZNER rejects values that fit into 255 bytes w/o quotes,
// Cannot use `in.GetTargetCombined()` for TXTs: // but do not fit w/ added quotes (via GetTargetCombined()).
// Their validation would complain about a missing `;`. // Sending the raw, non-quoted value works for the comprehensive
// Test case: single_TXT:Create_a_255-byte_TXT // suite of integrations tests.
// The HETZNER validation does not provide helpful error messages.
// {"error":{"message":"422 Unprocessable Entity: missing: ; ","code":422}} // {"error":{"message":"422 Unprocessable Entity: missing: ; ","code":422}}
record.Value = in.GetTargetField() valueNotQuoted := in.TxtStrings[0]
default: if len(valueNotQuoted) == 254 || len(valueNotQuoted) == 255 {
record.Value = in.GetTargetCombined() record.Value = valueNotQuoted
}
} }
return record return record
@ -90,7 +93,15 @@ func toRecordConfig(domain string, record *record) *models.RecordConfig {
} }
rc.SetLabel(record.Name, domain) rc.SetLabel(record.Name, domain)
_ = rc.PopulateFromString(record.Type, record.Value, domain) value := record.Value
// HACK: Hetzner is inserting a trailing space after multiple, quoted values.
// NOTE: The actual DNS answer does not contain the space.
if record.Type == "TXT" && len(value) > 0 && value[len(value)-1] == ' ' {
// Per RFC 1035 spaces outside quoted values are irrelevant.
value = strings.TrimRight(value, " ")
}
_ = rc.PopulateFromString(record.Type, value, domain)
return rc return rc
} }

View File

@ -0,0 +1,33 @@
package hexonet
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.TxtNoLongStrings(records); err != nil {
return err
}
// Still needed as of 2021-03-07
if err := recordaudit.TxtNoMultipleStrings(records); err != nil {
return err
}
// Still needed as of 2021-03-07
if err := recordaudit.TxtNotEmpty(records); err != nil {
return err
}
// Still needed as of 2021-03-01
if err := recordaudit.TxtNoTrailingSpace(records); err != nil {
return err
}
// Still needed as of 2021-03-01
return nil
}

View File

@ -25,7 +25,6 @@ var features = providers.DocumentationNotes{
providers.CanUseRoute53Alias: providers.Cannot("Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us."), providers.CanUseRoute53Alias: providers.Cannot("Using ALIAS is possible through our extended DNS (X-DNS) service. Feel free to get in touch with us."),
providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported"), providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported"),
providers.CanUseTLSA: providers.Can(), providers.CanUseTLSA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.CantUseNOPURGE: providers.Can(), providers.CantUseNOPURGE: providers.Can(),
providers.DocCreateDomains: providers.Can(), providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(), providers.DocDualHost: providers.Can(),
@ -67,6 +66,10 @@ func newDsp(conf map[string]string, meta json.RawMessage) (providers.DNSServiceP
} }
func init() { func init() {
fns := providers.DspFuncs{
Initializer: newDsp,
AuditRecordsor: AuditRecords,
}
providers.RegisterRegistrarType("HEXONET", newReg) providers.RegisterRegistrarType("HEXONET", newReg)
providers.RegisterDomainServiceProviderType("HEXONET", newDsp, features) providers.RegisterDomainServiceProviderType("HEXONET", fns, features)
} }

View File

@ -0,0 +1,11 @@
package internetbs
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -0,0 +1,33 @@
package inwx
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.TxtNoBackticks(records); err != nil {
return err
}
// Still needed as of 2021-03-01
if err := recordaudit.TxtNoLen255(records); err != nil {
return err
}
// Still needed as of 2021-03-01
if err := recordaudit.TxtNoTrailingSpace(records); err != nil {
return err
}
// Still needed as of 2021-03-01
if err := recordaudit.TxtNotEmpty(records); err != nil {
return err
}
// Still needed as of 2021-03-01
return nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
"github.com/nrdcg/goinwx" "github.com/nrdcg/goinwx"
@ -49,7 +50,6 @@ var features = providers.DocumentationNotes{
providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported."), providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported."),
providers.CanUseSSHFP: providers.Can(), providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(), providers.CanUseTLSA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.CanAutoDNSSEC: providers.Unimplemented("Supported by INWX but not implemented yet."), providers.CanAutoDNSSEC: providers.Unimplemented("Supported by INWX but not implemented yet."),
providers.DocOfficiallySupported: providers.Cannot(), providers.DocOfficiallySupported: providers.Cannot(),
providers.DocDualHost: providers.Can(), providers.DocDualHost: providers.Can(),
@ -68,7 +68,11 @@ type inwxAPI struct {
// init registers the registrar and the domain service provider with dnscontrol. // init registers the registrar and the domain service provider with dnscontrol.
func init() { func init() {
providers.RegisterRegistrarType("INWX", newInwxReg) providers.RegisterRegistrarType("INWX", newInwxReg)
providers.RegisterDomainServiceProviderType("INWX", newInwxDsp, features) fns := providers.DspFuncs{
Initializer: newInwxDsp,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("INWX", fns, features)
} }
// getOTP either returns the TOTPValue or uses TOTPKey and the current time to generate a valid TOTPValue. // getOTP either returns the TOTPValue or uses TOTPKey and the current time to generate a valid TOTPValue.
@ -231,6 +235,7 @@ func (api *inwxAPI) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Cor
} }
models.PostProcessRecords(foundRecords) models.PostProcessRecords(foundRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
err = checkRecords(dc.Records) err = checkRecords(dc.Records)
if err != nil { if err != nil {

View File

@ -0,0 +1,11 @@
package linode
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -93,7 +93,11 @@ var features = providers.DocumentationNotes{
func init() { func init() {
// SRV support is in this provider, but Linode doesn't seem to support it properly // SRV support is in this provider, but Linode doesn't seem to support it properly
providers.RegisterDomainServiceProviderType("LINODE", NewLinode, features) fns := providers.DspFuncs{
Initializer: NewLinode,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("LINODE", fns, features)
} }
// GetNameservers returns the nameservers for a domain. // GetNameservers returns the nameservers for a domain.

View File

@ -0,0 +1,43 @@
package msdns
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 2021-03-01
if err := recordaudit.TxtNotEmpty(records); err != nil {
return err
}
// Still needed as of 2021-03-01
if err := recordaudit.TxtNoBackticks(records); err != nil {
return err
}
// Still needed as of 2021-03-01
if err := recordaudit.TxtNoDoubleQuotes(records); err != nil {
return err
}
// Still needed as of 2021-03-01
if err := recordaudit.TxtNoSingleQuotes(records); err != nil {
return err
}
// Still needed as of 2021-03-01
if err := recordaudit.TxtNoLen255(records); err != nil {
return err
}
// Still needed as of 2021-03-01
return nil
}

View File

@ -5,6 +5,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
) )
// GetDomainCorrections gets existing records, diffs them against existing, and returns corrections. // GetDomainCorrections gets existing records, diffs them against existing, and returns corrections.
@ -18,6 +19,7 @@ func (c *msdnsProvider) GenerateDomainCorrections(dc *models.DomainConfig, exist
// Normalize // Normalize
models.PostProcessRecords(foundRecords) models.PostProcessRecords(foundRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc) differ := diff.New(dc)
_, creates, dels, modifications, err := differ.IncrementalDiff(foundRecords) _, creates, dels, modifications, err := differ.IncrementalDiff(foundRecords)

View File

@ -6,6 +6,7 @@ import (
"runtime" "runtime"
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
) )
@ -24,7 +25,6 @@ var features = providers.DocumentationNotes{
providers.CanUsePTR: providers.Can(), providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseTLSA: providers.Unimplemented(), providers.CanUseTLSA: providers.Unimplemented(),
providers.CanUseTXTMulti: providers.Unimplemented(),
providers.DocCreateDomains: providers.Cannot("This provider assumes the zone already existing on the dns server"), providers.DocCreateDomains: providers.Cannot("This provider assumes the zone already existing on the dns server"),
providers.DocDualHost: providers.Cannot("This driver does not manage NS records, so should not be used for dual-host scenarios"), providers.DocDualHost: providers.Cannot("This driver does not manage NS records, so should not be used for dual-host scenarios"),
providers.DocOfficiallySupported: providers.Can(), providers.DocOfficiallySupported: providers.Can(),
@ -33,7 +33,11 @@ var features = providers.DocumentationNotes{
// Register with the dnscontrol system. // Register with the dnscontrol system.
// This establishes the name (all caps), and the function to call to initialize it. // This establishes the name (all caps), and the function to call to initialize it.
func init() { func init() {
providers.RegisterDomainServiceProviderType("MSDNS", newDNS, features) fns := providers.DspFuncs{
Initializer: newDNS,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("MSDNS", fns, features)
} }
func newDNS(config map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { func newDNS(config map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
@ -76,6 +80,8 @@ func (client *msdnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
return nil, err return nil, err
} }
models.PostProcessRecords(existing) models.PostProcessRecords(existing)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
clean := PrepFoundRecords(existing) clean := PrepFoundRecords(existing)
PrepDesiredRecords(dc) PrepDesiredRecords(dc)
return client.GenerateDomainCorrections(dc, clean) return client.GenerateDomainCorrections(dc, clean)

View File

@ -162,6 +162,8 @@ func generatePSDelete(dnsserver, domain string, rec *models.RecordConfig) string
fmt.Fprintf(&b, ` -RRType "%s"`, rec.Type) fmt.Fprintf(&b, ` -RRType "%s"`, rec.Type)
if rec.Type == "MX" { if rec.Type == "MX" {
fmt.Fprintf(&b, ` -RecordData %d,"%s"`, rec.MxPreference, rec.GetTargetField()) fmt.Fprintf(&b, ` -RecordData %d,"%s"`, rec.MxPreference, rec.GetTargetField())
} else if rec.Type == "TXT" {
fmt.Fprintf(&b, ` -RecordData %s`, rec.GetTargetField())
} else if rec.Type == "SRV" { } else if rec.Type == "SRV" {
// https://www.gitmemory.com/issue/MicrosoftDocs/windows-powershell-docs/1149/511916884 // https://www.gitmemory.com/issue/MicrosoftDocs/windows-powershell-docs/1149/511916884
fmt.Fprintf(&b, ` -RecordData %d,%d,%d,"%s"`, rec.SrvPriority, rec.SrvWeight, rec.SrvPort, rec.GetTargetField()) fmt.Fprintf(&b, ` -RecordData %d,%d,%d,"%s"`, rec.SrvPriority, rec.SrvWeight, rec.SrvPort, rec.GetTargetField())
@ -214,7 +216,9 @@ func generatePSCreate(dnsserver, domain string, rec *models.RecordConfig) string
//case "WKS": //case "WKS":
// fmt.Fprintf(&b, ` -Wks -InternetAddress <IPAddress> -InternetProtocol {UDP | TCP} -Service <String[]>`, rec.GetTargetField()) // fmt.Fprintf(&b, ` -Wks -InternetAddress <IPAddress> -InternetProtocol {UDP | TCP} -Service <String[]>`, rec.GetTargetField())
case "TXT": case "TXT":
fmt.Fprintf(&b, ` -Txt -DescriptiveText "%s"`, rec.GetTargetField()) fmt.Printf("DEBUG TXT len = %v\n", rec.TxtStrings)
fmt.Printf("DEBUG TXT target = %q\n", rec.GetTargetField())
fmt.Fprintf(&b, ` -Txt -DescriptiveText %s`, rec.GetTargetField())
//case "RT": //case "RT":
// fmt.Fprintf(&b, ` -RT -IntermediateHost <String> -Preference <UInt16>`, rec.GetTargetField()) // fmt.Fprintf(&b, ` -RT -IntermediateHost <String> -Preference <UInt16>`, rec.GetTargetField())
//case "RP": //case "RP":

View File

@ -0,0 +1,11 @@
package namecheap
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -41,7 +41,11 @@ var features = providers.DocumentationNotes{
func init() { func init() {
providers.RegisterRegistrarType("NAMECHEAP", newReg) providers.RegisterRegistrarType("NAMECHEAP", newReg)
providers.RegisterDomainServiceProviderType("NAMECHEAP", newDsp, features) fns := providers.DspFuncs{
Initializer: newDsp,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("NAMECHEAP", fns, features)
providers.RegisterCustomRecordType("URL", "NAMECHEAP", "") providers.RegisterCustomRecordType("URL", "NAMECHEAP", "")
providers.RegisterCustomRecordType("URL301", "NAMECHEAP", "") providers.RegisterCustomRecordType("URL301", "NAMECHEAP", "")
providers.RegisterCustomRecordType("FRAME", "NAMECHEAP", "") providers.RegisterCustomRecordType("FRAME", "NAMECHEAP", "")

View File

@ -0,0 +1,41 @@
package namedotcom
import (
"fmt"
"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 := MaxLengthNDC(records); err != nil {
return err
}
// Still needed as of 2021-03-01
if err := recordaudit.TxtNotEmpty(records); err != nil {
return err
}
// Still needed as of 2021-03-01
return nil
}
// MaxLengthNDC returns and error if the sum of the strings
// are longer than permitted by DigitalOcean. Sadly their
// length limit is undocumented. This seems to work.
func MaxLengthNDC(records []*models.RecordConfig) error {
for _, rc := range records {
if rc.HasFormatIdenticalToTXT() { // TXT and similar:
if len(rc.GetTargetField()) > 512 {
return fmt.Errorf("encoded txt too long")
}
}
}
return nil
}

View File

@ -24,7 +24,6 @@ var features = providers.DocumentationNotes{
providers.CanUseAlias: providers.Can(), providers.CanUseAlias: providers.Can(),
providers.CanUsePTR: providers.Cannot("PTR records are not supported (See Link)", "https://www.name.com/support/articles/205188508-Reverse-DNS-records"), providers.CanUsePTR: providers.Cannot("PTR records are not supported (See Link)", "https://www.name.com/support/articles/205188508-Reverse-DNS-records"),
providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported"), providers.CanUseSRV: providers.Can("SRV records with empty targets are not supported"),
providers.CanUseTXTMulti: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot("New domains require registration"), providers.DocCreateDomains: providers.Cannot("New domains require registration"),
providers.DocDualHost: providers.Cannot("Apex NS records not editable"), providers.DocDualHost: providers.Cannot("Apex NS records not editable"),
providers.DocOfficiallySupported: providers.Can(), providers.DocOfficiallySupported: providers.Can(),
@ -56,5 +55,9 @@ func newProvider(conf map[string]string) (*namedotcomProvider, error) {
func init() { func init() {
providers.RegisterRegistrarType("NAMEDOTCOM", newReg) providers.RegisterRegistrarType("NAMEDOTCOM", newReg)
providers.RegisterDomainServiceProviderType("NAMEDOTCOM", newDsp, features) fns := providers.DspFuncs{
Initializer: newDsp,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("NAMEDOTCOM", fns, features)
} }

View File

@ -170,8 +170,8 @@ func (n *namedotcomProvider) createRecord(rc *models.RecordConfig, domain string
switch rc.Type { // #rtype_variations switch rc.Type { // #rtype_variations
case "A", "AAAA", "ANAME", "CNAME", "MX", "NS": case "A", "AAAA", "ANAME", "CNAME", "MX", "NS":
// nothing // nothing
case "TXT": case "TXT":
record.Answer = encodeTxt(rc.TxtStrings) // record.Answer = encodeTxt(rc.TxtStrings)
case "SRV": case "SRV":
if rc.GetTargetField() == "." { if rc.GetTargetField() == "." {
return errors.New("SRV records with empty targets are not supported (as of 2019-11-05, the API returns 'Parameter Value Error - Invalid Srv Format')") return errors.New("SRV records with empty targets are not supported (as of 2019-11-05, the API returns 'Parameter Value Error - Invalid Srv Format')")
@ -187,18 +187,18 @@ func (n *namedotcomProvider) createRecord(rc *models.RecordConfig, domain string
return err return err
} }
// makeTxt encodes TxtStrings for sending in the CREATE/MODIFY API: // // makeTxt encodes TxtStrings for sending in the CREATE/MODIFY API:
func encodeTxt(txts []string) string { // func encodeTxt(txts []string) string {
ans := txts[0] // ans := txts[0]
if len(txts) > 1 { // if len(txts) > 1 {
ans = "" // ans = ""
for _, t := range txts { // for _, t := range txts {
ans += `"` + strings.Replace(t, `"`, `\"`, -1) + `"` // ans += `"` + strings.Replace(t, `"`, `\"`, -1) + `"`
} // }
} // }
return ans // return ans
} // }
// finds a string surrounded by quotes that might contain an escaped quote character. // finds a string surrounded by quotes that might contain an escaped quote character.
var quotedStringRegexp = regexp.MustCompile(`"((?:[^"\\]|\\.)*)"`) var quotedStringRegexp = regexp.MustCompile(`"((?:[^"\\]|\\.)*)"`)

View File

@ -21,16 +21,16 @@ var txtData = []struct {
{[]string{"eh", "bzz", "cee"}, `"eh""bzz""cee"`}, {[]string{"eh", "bzz", "cee"}, `"eh""bzz""cee"`},
} }
func TestEncodeTxt(t *testing.T) { // func TestEncodeTxt(t *testing.T) {
// Test encoded the lists of strings into a string: // // Test encoded the lists of strings into a string:
for i, test := range txtData { // for i, test := range txtData {
enc := encodeTxt(test.decoded) // enc := encodeTxt(test.decoded)
if enc != test.encoded { // if enc != test.encoded {
t.Errorf("%v: txt\n data: []string{%v}\nexpected: %s\n got: %s", // t.Errorf("%v: txt\n data: []string{%v}\nexpected: %s\n got: %s",
i, "`"+strings.Join(test.decoded, "`, `")+"`", test.encoded, enc) // i, "`"+strings.Join(test.decoded, "`, `")+"`", test.encoded, enc)
} // }
} // }
} //}
func TestDecodeTxt(t *testing.T) { func TestDecodeTxt(t *testing.T) {
// Test decoded a string into the list of strings: // Test decoded a string into the list of strings:

View File

@ -0,0 +1,13 @@
package netcup
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 {
return recordaudit.TxtNotEmpty(records)
// Still needed as of 2021-03-01
}

View File

@ -6,6 +6,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
) )
@ -16,12 +17,15 @@ var features = providers.DocumentationNotes{
providers.CanUsePTR: providers.Cannot(), providers.CanUsePTR: providers.Cannot(),
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseCAA: providers.Can(), providers.CanUseCAA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.CanGetZones: providers.Cannot(), providers.CanGetZones: providers.Cannot(),
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("NETCUP", New, features) fns := providers.DspFuncs{
Initializer: New,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("NETCUP", fns, features)
} }
// New creates a new API handle. // New creates a new API handle.
@ -94,6 +98,8 @@ func (api *netcupProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mod
// Normalize // Normalize
models.PostProcessRecords(existingRecords) models.PostProcessRecords(existingRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc) differ := diff.New(dc)
_, create, del, modify, err := differ.IncrementalDiff(existingRecords) _, create, del, modify, err := differ.IncrementalDiff(existingRecords)
if err != nil { if err != nil {

View File

@ -0,0 +1,18 @@
package ns1
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 2021-03-01
return nil
}

View File

@ -18,14 +18,17 @@ import (
var docNotes = providers.DocumentationNotes{ var docNotes = providers.DocumentationNotes{
providers.CanUseAlias: providers.Can(), providers.CanUseAlias: providers.Can(),
providers.CanUsePTR: providers.Can(), providers.CanUsePTR: providers.Can(),
providers.CanUseTXTMulti: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot(), providers.DocCreateDomains: providers.Cannot(),
providers.DocOfficiallySupported: providers.Cannot(), providers.DocOfficiallySupported: providers.Cannot(),
providers.DocDualHost: providers.Can(), providers.DocDualHost: providers.Can(),
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("NS1", newProvider, providers.CanUseSRV, docNotes) fns := providers.DspFuncs{
Initializer: newProvider,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("NS1", fns, providers.CanUseSRV, docNotes)
providers.RegisterCustomRecordType("NS1_URLFWD", "NS1", "URLFWD") providers.RegisterCustomRecordType("NS1_URLFWD", "NS1", "URLFWD")
} }

View File

@ -0,0 +1,11 @@
package octodns
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -29,6 +29,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
"github.com/StackExchange/dnscontrol/v3/providers/octodns/octoyaml" "github.com/StackExchange/dnscontrol/v3/providers/octodns/octoyaml"
) )
@ -37,7 +38,6 @@ var features = providers.DocumentationNotes{
//providers.CanUseCAA: providers.Can(), //providers.CanUseCAA: providers.Can(),
providers.CanUsePTR: providers.Can(), providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
//providers.CanUseTXTMulti: providers.Can(),
providers.DocCreateDomains: providers.Cannot("Driver just maintains list of OctoDNS config files. You must manually create the master config files that refer these."), providers.DocCreateDomains: providers.Cannot("Driver just maintains list of OctoDNS config files. You must manually create the master config files that refer these."),
providers.DocDualHost: providers.Cannot("Research is needed."), providers.DocDualHost: providers.Cannot("Research is needed."),
providers.CanGetZones: providers.Unimplemented(), providers.CanGetZones: providers.Unimplemented(),
@ -63,7 +63,11 @@ func initProvider(config map[string]string, providermeta json.RawMessage) (provi
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("OCTODNS", initProvider, features) fns := providers.DspFuncs{
Initializer: initProvider,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("OCTODNS", fns, features)
} }
// octodnsProvider is the provider handle for the OctoDNS driver. // octodnsProvider is the provider handle for the OctoDNS driver.
@ -123,6 +127,7 @@ func (c *octodnsProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
// Normalize // Normalize
models.PostProcessRecords(foundRecords) models.PostProcessRecords(foundRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc) differ := diff.New(dc)
_, create, del, mod, err := differ.IncrementalDiff(foundRecords) _, create, del, mod, err := differ.IncrementalDiff(foundRecords)

View File

@ -0,0 +1,11 @@
package opensrs
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -0,0 +1,11 @@
package oracle
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -14,6 +14,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/printer" "github.com/StackExchange/dnscontrol/v3/pkg/printer"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
) )
@ -31,11 +32,14 @@ var features = providers.DocumentationNotes{
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(), providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(), providers.CanUseTLSA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("ORACLE", New, features) fns := providers.DspFuncs{
Initializer: New,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("ORACLE", fns, features)
} }
type oracleProvider struct { type oracleProvider struct {
@ -218,6 +222,7 @@ func (o *oracleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*model
// Normalize // Normalize
models.PostProcessRecords(existingRecords) models.PostProcessRecords(existingRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
filteredNewRecords := models.Records{} filteredNewRecords := models.Records{}

View File

@ -0,0 +1,11 @@
package ovh
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -8,6 +8,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
"github.com/ovh/go-ovh/ovh" "github.com/ovh/go-ovh/ovh"
) )
@ -28,7 +29,6 @@ var features = providers.DocumentationNotes{
providers.DocDualHost: providers.Can(), providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(), providers.DocOfficiallySupported: providers.Cannot(),
providers.CanGetZones: providers.Can(), providers.CanGetZones: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
} }
func newOVH(m map[string]string, metadata json.RawMessage) (*ovhProvider, error) { func newOVH(m map[string]string, metadata json.RawMessage) (*ovhProvider, error) {
@ -55,8 +55,12 @@ func newReg(conf map[string]string) (providers.Registrar, error) {
} }
func init() { func init() {
fns := providers.DspFuncs{
Initializer: newDsp,
AuditRecordsor: AuditRecords,
}
providers.RegisterRegistrarType("OVH", newReg) providers.RegisterRegistrarType("OVH", newReg)
providers.RegisterDomainServiceProviderType("OVH", newDsp, features) providers.RegisterDomainServiceProviderType("OVH", fns, features)
} }
func (c *ovhProvider) GetNameservers(domain string) ([]*models.Nameserver, error) { func (c *ovhProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
@ -116,6 +120,7 @@ func (c *ovhProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.C
// Normalize // Normalize
models.PostProcessRecords(actual) models.PostProcessRecords(actual)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
differ := diff.New(dc) differ := diff.New(dc)
_, create, delete, modify, err := differ.IncrementalDiff(actual) _, create, delete, modify, err := differ.IncrementalDiff(actual)

View File

@ -0,0 +1,11 @@
package powerdns
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -27,13 +27,16 @@ var features = providers.DocumentationNotes{
providers.DocCreateDomains: providers.Can(), providers.DocCreateDomains: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(), providers.DocOfficiallySupported: providers.Cannot(),
providers.CanGetZones: providers.Can(), providers.CanGetZones: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.DocDualHost: providers.Can(), providers.DocDualHost: providers.Can(),
providers.CanUseNAPTR: providers.Can(), providers.CanUseNAPTR: providers.Can(),
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("POWERDNS", NewProvider, features) fns := providers.DspFuncs{
Initializer: NewProvider,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("POWERDNS", fns, features)
} }
// powerdnsProvider represents the powerdnsProvider DNSServiceProvider. // powerdnsProvider represents the powerdnsProvider DNSServiceProvider.

View File

@ -40,8 +40,19 @@ var RegistrarTypes = map[string]RegistrarInitializer{}
// DspInitializer is a function to create a DNS service provider. Function will be passed the unprocessed json payload from the configuration file for the given provider. // DspInitializer is a function to create a DNS service provider. Function will be passed the unprocessed json payload from the configuration file for the given provider.
type DspInitializer func(map[string]string, json.RawMessage) (DNSServiceProvider, error) type DspInitializer func(map[string]string, json.RawMessage) (DNSServiceProvider, error)
// AuditRecordsor is a function that verifies that all the records
// are supportable by this provider. It returns an error related to
// the first record that this provider can not support.
type AuditRecordsor func([]*models.RecordConfig) error
// DspFuncs lists functions registered with a provider.
type DspFuncs struct {
Initializer DspInitializer
AuditRecordsor AuditRecordsor
}
// DNSProviderTypes stores initializer for each DSP. // DNSProviderTypes stores initializer for each DSP.
var DNSProviderTypes = map[string]DspInitializer{} var DNSProviderTypes = map[string]DspFuncs{}
// RegisterRegistrarType adds a registrar type to the registry by providing a suitable initialization function. // RegisterRegistrarType adds a registrar type to the registry by providing a suitable initialization function.
func RegisterRegistrarType(name string, init RegistrarInitializer, pm ...ProviderMetadata) { func RegisterRegistrarType(name string, init RegistrarInitializer, pm ...ProviderMetadata) {
@ -53,11 +64,11 @@ func RegisterRegistrarType(name string, init RegistrarInitializer, pm ...Provide
} }
// RegisterDomainServiceProviderType adds a dsp to the registry with the given initialization function. // RegisterDomainServiceProviderType adds a dsp to the registry with the given initialization function.
func RegisterDomainServiceProviderType(name string, init DspInitializer, pm ...ProviderMetadata) { func RegisterDomainServiceProviderType(name string, fns DspFuncs, pm ...ProviderMetadata) {
if _, ok := DNSProviderTypes[name]; ok { if _, ok := DNSProviderTypes[name]; ok {
log.Fatalf("Cannot register registrar type %s multiple times", name) log.Fatalf("Cannot register registrar type %s multiple times", name)
} }
DNSProviderTypes[name] = init DNSProviderTypes[name] = fns
unwrapProviderCapabilities(name, pm) unwrapProviderCapabilities(name, pm)
} }
@ -72,11 +83,22 @@ func CreateRegistrar(rType string, config map[string]string) (Registrar, error)
// CreateDNSProvider initializes a dns provider instance from given credentials. // CreateDNSProvider initializes a dns provider instance from given credentials.
func CreateDNSProvider(dType string, config map[string]string, meta json.RawMessage) (DNSServiceProvider, error) { func CreateDNSProvider(dType string, config map[string]string, meta json.RawMessage) (DNSServiceProvider, error) {
initer, ok := DNSProviderTypes[dType] p, ok := DNSProviderTypes[dType]
if !ok { if !ok {
return nil, fmt.Errorf("DSP type %s not declared", dType) return nil, fmt.Errorf("DSP type %s not declared", dType)
} }
return initer(config, meta) return p.Initializer(config, meta)
}
func AuditRecords(dType string, rcs models.Records) error {
p, ok := DNSProviderTypes[dType]
if !ok {
return fmt.Errorf("DSP type %s not declared", dType)
}
if p.AuditRecordsor == nil {
return fmt.Errorf("DSP type %s has no AuditRecordsor", dType)
}
return p.AuditRecordsor(rcs)
} }
// None is a basic provider type that does absolutely nothing. Can be useful as a placeholder for third parties or unimplemented providers. // None is a basic provider type that does absolutely nothing. Can be useful as a placeholder for third parties or unimplemented providers.

View File

@ -0,0 +1,11 @@
package route53
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -17,6 +17,7 @@ import (
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff" "github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/txtutil"
"github.com/StackExchange/dnscontrol/v3/providers" "github.com/StackExchange/dnscontrol/v3/providers"
) )
@ -72,14 +73,17 @@ var features = providers.DocumentationNotes{
providers.DocOfficiallySupported: providers.Can(), providers.DocOfficiallySupported: providers.Can(),
providers.CanUsePTR: providers.Can(), providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(), providers.CanUseSRV: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
providers.CanUseCAA: providers.Can(), providers.CanUseCAA: providers.Can(),
providers.CanUseRoute53Alias: providers.Can(), providers.CanUseRoute53Alias: providers.Can(),
providers.CanGetZones: providers.Can(), providers.CanGetZones: providers.Can(),
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("ROUTE53", newRoute53Dsp, features) fns := providers.DspFuncs{
Initializer: newRoute53Dsp,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("ROUTE53", fns, features)
providers.RegisterRegistrarType("ROUTE53", newRoute53Reg) providers.RegisterRegistrarType("ROUTE53", newRoute53Reg)
providers.RegisterCustomRecordType("R53_ALIAS", "ROUTE53", "") providers.RegisterCustomRecordType("R53_ALIAS", "ROUTE53", "")
} }
@ -232,6 +236,7 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
// Normalize // Normalize
models.PostProcessRecords(existingRecords) models.PostProcessRecords(existingRecords)
txtutil.SplitSingleLongTxt(dc.Records) // Autosplit long TXT records
// diff // diff
differ := diff.New(dc, getAliasMap) differ := diff.New(dc, getAliasMap)

View File

@ -0,0 +1,11 @@
package softlayer
import (
"github.com/StackExchange/dnscontrol/v3/models"
)
// AuditRecords returns an error if any records are not
// supportable by this provider.
func AuditRecords(records []*models.RecordConfig) error {
return nil
}

View File

@ -27,7 +27,11 @@ var features = providers.DocumentationNotes{
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("SOFTLAYER", newReg, features) fns := providers.DspFuncs{
Initializer: newReg,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("SOFTLAYER", fns, features)
} }
func newReg(conf map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) { func newReg(conf map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {

View File

@ -0,0 +1,20 @@
package vultr
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 {
// TODO(tlim) Needs investigation. Could be a dnscontrol issue or
// the provider doesn't support double quotes.
if err := recordaudit.TxtNoDoubleQuotes(records); err != nil {
return err
}
// Still needed as of 2021-03-02
return nil
}

View File

@ -37,7 +37,11 @@ var features = providers.DocumentationNotes{
} }
func init() { func init() {
providers.RegisterDomainServiceProviderType("VULTR", NewProvider, features) fns := providers.DspFuncs{
Initializer: NewProvider,
AuditRecordsor: AuditRecords,
}
providers.RegisterDomainServiceProviderType("VULTR", fns, features)
} }
// vultrProvider represents the Vultr DNSServiceProvider. // vultrProvider represents the Vultr DNSServiceProvider.
@ -220,10 +224,6 @@ func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig, vultrID int
if strings.HasSuffix(data, ".") { if strings.HasSuffix(data, ".") {
data = data[:len(data)-1] data = data[:len(data)-1]
} }
// Vultr needs TXT record in quotes.
if rc.Type == "TXT" {
data = fmt.Sprintf(`"%s"`, data)
}
var priority *int var priority *int