mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
Add "get-zone" command (#613)
* Add GetZoneRecords to DNSProvider interface * dnscontrol now uses ufave/cli/v2 * NEW: get-zones.md * HasRecordTypeName should be a method on models.Records not models.DomainConfig * Implement BIND's GetZoneRecords * new WriteZoneFile implemented * go mod vendor * Update docs to use get-zone instead of convertzone * Add CanGetZone capability and update all providers. * Get all zones for a provider at once (#626) * implement GetZoneRecords for cloudflare * munge cloudflare ttls * Implement GetZoneRecords for cloudflare (#625) Co-authored-by: Craig Peterson <192540+captncraig@users.noreply.github.com>
This commit is contained in:
142
pkg/prettyzone/prettyzone.go
Normal file
142
pkg/prettyzone/prettyzone.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package prettyzone
|
||||
|
||||
// Generate zonefiles.
|
||||
// This generates a zonefile that prioritizes beauty over efficiency.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v2/models"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// mostCommonTTL returns the most common TTL in a set of records. If there is
|
||||
// a tie, the highest TTL is selected. This makes the results consistent.
|
||||
// NS records are not included in the analysis because Tom said so.
|
||||
func mostCommonTTL(records models.Records) uint32 {
|
||||
// Index the TTLs in use:
|
||||
d := make(map[uint32]int)
|
||||
for _, r := range records {
|
||||
if r.Type != "NS" {
|
||||
d[r.TTL]++
|
||||
}
|
||||
}
|
||||
// Find the largest count:
|
||||
var mc int
|
||||
for _, value := range d {
|
||||
if value > mc {
|
||||
mc = value
|
||||
}
|
||||
}
|
||||
// Find the largest key with that count:
|
||||
var mk uint32
|
||||
for key, value := range d {
|
||||
if value == mc {
|
||||
if key > mk {
|
||||
mk = key
|
||||
}
|
||||
}
|
||||
}
|
||||
return mk
|
||||
}
|
||||
|
||||
// WriteZoneFileRR is a helper for when you have []dns.RR instead of models.Records
|
||||
func WriteZoneFileRR(w io.Writer, records []dns.RR, origin string, serial uint32) error {
|
||||
return WriteZoneFileRC(w, models.RRstoRCs(records, origin, serial), origin)
|
||||
}
|
||||
|
||||
// WriteZoneFileRC writes a beautifully formatted zone file.
|
||||
func WriteZoneFileRC(w io.Writer, records models.Records, origin string) error {
|
||||
// This function prioritizes beauty over output size.
|
||||
// * The zone records are sorted by label, grouped by subzones to
|
||||
// be easy to read and pleasant to the eye.
|
||||
// * Within a label, SOA and NS records are listed first.
|
||||
// * MX records are sorted numericly by preference value.
|
||||
// * SRV records are sorted numericly by port, then priority, then weight.
|
||||
// * A records are sorted by IP address, not lexicographically.
|
||||
// * Repeated labels are removed.
|
||||
// * $TTL is used to eliminate clutter. The most common TTL value is used.
|
||||
// * "@" is used instead of the apex domain name.
|
||||
|
||||
z := PrettySort(records, origin, mostCommonTTL(records))
|
||||
|
||||
return z.generateZoneFileHelper(w)
|
||||
}
|
||||
|
||||
func PrettySort(records models.Records, origin string, defaultTTL uint32) *zoneGenData {
|
||||
if defaultTTL == 0 {
|
||||
defaultTTL = mostCommonTTL(records)
|
||||
}
|
||||
z := &zoneGenData{
|
||||
Origin: origin + ".",
|
||||
DefaultTTL: defaultTTL,
|
||||
}
|
||||
z.Records = nil
|
||||
for _, r := range records {
|
||||
z.Records = append(z.Records, r)
|
||||
}
|
||||
return z
|
||||
}
|
||||
|
||||
// generateZoneFileHelper creates a pretty zonefile.
|
||||
func (z *zoneGenData) generateZoneFileHelper(w io.Writer) error {
|
||||
|
||||
nameShortPrevious := ""
|
||||
|
||||
sort.Sort(z)
|
||||
fmt.Fprintln(w, "$TTL", z.DefaultTTL)
|
||||
for i, rr := range z.Records {
|
||||
|
||||
// Fake types are commented out.
|
||||
prefix := ""
|
||||
_, ok := dns.StringToType[rr.Type]
|
||||
if !ok {
|
||||
prefix = ";"
|
||||
}
|
||||
|
||||
// name
|
||||
nameShort := rr.Name
|
||||
name := nameShort
|
||||
if (prefix == "") && (i > 0 && nameShort == nameShortPrevious) {
|
||||
name = ""
|
||||
} else {
|
||||
name = nameShort
|
||||
}
|
||||
nameShortPrevious = nameShort
|
||||
|
||||
// ttl
|
||||
ttl := ""
|
||||
if rr.TTL != z.DefaultTTL && rr.TTL != 0 {
|
||||
ttl = fmt.Sprint(rr.TTL)
|
||||
}
|
||||
|
||||
// type
|
||||
typeStr := rr.Type
|
||||
|
||||
// the remaining line
|
||||
target := rr.GetTargetCombined()
|
||||
|
||||
fmt.Fprintf(w, "%s%s\n",
|
||||
prefix, formatLine([]int{10, 5, 2, 5, 0}, []string{name, ttl, "IN", typeStr, target}))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatLine(lengths []int, fields []string) string {
|
||||
c := 0
|
||||
result := ""
|
||||
for i, length := range lengths {
|
||||
item := fields[i]
|
||||
for len(result) < c {
|
||||
result += " "
|
||||
}
|
||||
if item != "" {
|
||||
result += item + " "
|
||||
}
|
||||
c += length + 1
|
||||
}
|
||||
return strings.TrimRight(result, " ")
|
||||
}
|
||||
527
pkg/prettyzone/prettyzone_test.go
Normal file
527
pkg/prettyzone/prettyzone_test.go
Normal file
@@ -0,0 +1,527 @@
|
||||
package prettyzone
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v2/models"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
)
|
||||
|
||||
func parseAndRegen(t *testing.T, buf *bytes.Buffer, expected string) {
|
||||
// Take a zonefile, parse it, then generate a zone. We should
|
||||
// get back the same string.
|
||||
// This is used after any WriteZoneFile test as an extra verification step.
|
||||
|
||||
// Parse the output:
|
||||
var parsed []dns.RR
|
||||
for x := range dns.ParseZone(buf, "bosun.org", "bosun.org.zone") {
|
||||
if x.Error != nil {
|
||||
log.Fatalf("Error in zonefile: %v", x.Error)
|
||||
} else {
|
||||
parsed = append(parsed, x.RR)
|
||||
}
|
||||
}
|
||||
// Generate it back:
|
||||
buf2 := &bytes.Buffer{}
|
||||
WriteZoneFileRR(buf2, parsed, "bosun.org", 99)
|
||||
|
||||
// Compare:
|
||||
if buf2.String() != expected {
|
||||
t.Fatalf("Regenerated zonefile does not match: got=(\n%v\n)\nexpected=(\n%v\n)\n", buf2.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMostCommonTtl(t *testing.T) {
|
||||
var records []dns.RR
|
||||
var g, e uint32
|
||||
r1, _ := dns.NewRR("bosun.org. 100 IN A 1.1.1.1")
|
||||
r2, _ := dns.NewRR("bosun.org. 200 IN A 1.1.1.1")
|
||||
r3, _ := dns.NewRR("bosun.org. 300 IN A 1.1.1.1")
|
||||
r4, _ := dns.NewRR("bosun.org. 400 IN NS foo.bosun.org.")
|
||||
r5, _ := dns.NewRR("bosun.org. 400 IN NS bar.bosun.org.")
|
||||
|
||||
// All records are TTL=100
|
||||
records = nil
|
||||
records, e = append(records, r1, r1, r1), 100
|
||||
x := models.RRstoRCs(records, "bosun.org", 99)
|
||||
g = mostCommonTTL(x)
|
||||
if e != g {
|
||||
t.Fatalf("expected %d; got %d\n", e, g)
|
||||
}
|
||||
|
||||
// Mixture of TTLs with an obvious winner.
|
||||
records = nil
|
||||
records, e = append(records, r1, r2, r2), 200
|
||||
g = mostCommonTTL(models.RRstoRCs(records, "bosun.org", 99))
|
||||
if e != g {
|
||||
t.Fatalf("expected %d; got %d\n", e, g)
|
||||
}
|
||||
|
||||
// 3-way tie. Largest TTL should be used.
|
||||
records = nil
|
||||
records, e = append(records, r1, r2, r3), 300
|
||||
g = mostCommonTTL(models.RRstoRCs(records, "bosun.org", 99))
|
||||
if e != g {
|
||||
t.Fatalf("expected %d; got %d\n", e, g)
|
||||
}
|
||||
|
||||
// NS records are ignored.
|
||||
records = nil
|
||||
records, e = append(records, r1, r4, r5), 100
|
||||
g = mostCommonTTL(models.RRstoRCs(records, "bosun.org", 99))
|
||||
if e != g {
|
||||
t.Fatalf("expected %d; got %d\n", e, g)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// func WriteZoneFile
|
||||
|
||||
func TestWriteZoneFileSimple(t *testing.T) {
|
||||
r1, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.153")
|
||||
r2, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.154")
|
||||
r3, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
|
||||
buf := &bytes.Buffer{}
|
||||
WriteZoneFileRR(buf, []dns.RR{r1, r2, r3}, "bosun.org", 99)
|
||||
expected := `$TTL 300
|
||||
@ IN A 192.30.252.153
|
||||
IN A 192.30.252.154
|
||||
www IN CNAME bosun.org.
|
||||
`
|
||||
if buf.String() != expected {
|
||||
t.Log(buf.String())
|
||||
t.Log(expected)
|
||||
t.Fatalf("Zone file does not match.")
|
||||
}
|
||||
|
||||
parseAndRegen(t, buf, expected)
|
||||
}
|
||||
|
||||
func TestWriteZoneFileSimpleTtl(t *testing.T) {
|
||||
r1, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.153")
|
||||
r2, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.154")
|
||||
r3, _ := dns.NewRR("bosun.org. 100 IN A 192.30.252.155")
|
||||
r4, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
|
||||
buf := &bytes.Buffer{}
|
||||
WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4}, "bosun.org", 99)
|
||||
expected := `$TTL 100
|
||||
@ IN A 192.30.252.153
|
||||
IN A 192.30.252.154
|
||||
IN A 192.30.252.155
|
||||
www 300 IN CNAME bosun.org.
|
||||
`
|
||||
if buf.String() != expected {
|
||||
t.Log(buf.String())
|
||||
t.Log(expected)
|
||||
t.Fatalf("Zone file does not match")
|
||||
}
|
||||
|
||||
parseAndRegen(t, buf, expected)
|
||||
}
|
||||
|
||||
func TestWriteZoneFileMx(t *testing.T) {
|
||||
// sort by priority
|
||||
r1, _ := dns.NewRR("aaa.bosun.org. IN MX 1 aaa.example.com.")
|
||||
r2, _ := dns.NewRR("aaa.bosun.org. IN MX 5 aaa.example.com.")
|
||||
r3, _ := dns.NewRR("aaa.bosun.org. IN MX 10 aaa.example.com.")
|
||||
// same priority? sort by name
|
||||
r4, _ := dns.NewRR("bbb.bosun.org. IN MX 10 ccc.example.com.")
|
||||
r5, _ := dns.NewRR("bbb.bosun.org. IN MX 10 bbb.example.com.")
|
||||
r6, _ := dns.NewRR("bbb.bosun.org. IN MX 10 aaa.example.com.")
|
||||
// a mix
|
||||
r7, _ := dns.NewRR("ccc.bosun.org. IN MX 40 zzz.example.com.")
|
||||
r8, _ := dns.NewRR("ccc.bosun.org. IN MX 40 aaa.example.com.")
|
||||
r9, _ := dns.NewRR("ccc.bosun.org. IN MX 1 ttt.example.com.")
|
||||
buf := &bytes.Buffer{}
|
||||
WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5, r6, r7, r8, r9}, "bosun.org", 99)
|
||||
if buf.String() != testdataZFMX {
|
||||
t.Log(buf.String())
|
||||
t.Log(testdataZFMX)
|
||||
t.Fatalf("Zone file does not match.")
|
||||
}
|
||||
parseAndRegen(t, buf, testdataZFMX)
|
||||
}
|
||||
|
||||
var testdataZFMX = `$TTL 3600
|
||||
aaa IN MX 1 aaa.example.com.
|
||||
IN MX 5 aaa.example.com.
|
||||
IN MX 10 aaa.example.com.
|
||||
bbb IN MX 10 aaa.example.com.
|
||||
IN MX 10 bbb.example.com.
|
||||
IN MX 10 ccc.example.com.
|
||||
ccc IN MX 1 ttt.example.com.
|
||||
IN MX 40 aaa.example.com.
|
||||
IN MX 40 zzz.example.com.
|
||||
`
|
||||
|
||||
func TestWriteZoneFileSrv(t *testing.T) {
|
||||
// exhibits explicit ttls and long name
|
||||
r1, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 9999 foo.com.`)
|
||||
r2, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 20 5050 foo.com.`)
|
||||
r3, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 5050 foo.com.`)
|
||||
r4, _ := dns.NewRR(`bosun.org. 300 IN SRV 20 10 5050 foo.com.`)
|
||||
r5, _ := dns.NewRR(`bosun.org. 300 IN SRV 10 10 5050 foo.com.`)
|
||||
buf := &bytes.Buffer{}
|
||||
WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5}, "bosun.org", 99)
|
||||
if buf.String() != testdataZFSRV {
|
||||
t.Log(buf.String())
|
||||
t.Log(testdataZFSRV)
|
||||
t.Fatalf("Zone file does not match.")
|
||||
}
|
||||
parseAndRegen(t, buf, testdataZFSRV)
|
||||
}
|
||||
|
||||
var testdataZFSRV = `$TTL 300
|
||||
@ IN SRV 10 10 5050 foo.com.
|
||||
IN SRV 10 10 5050 foo.com.
|
||||
IN SRV 10 20 5050 foo.com.
|
||||
IN SRV 20 10 5050 foo.com.
|
||||
IN SRV 10 10 9999 foo.com.
|
||||
`
|
||||
|
||||
func TestWriteZoneFilePtr(t *testing.T) {
|
||||
// exhibits explicit ttls and long name
|
||||
r1, _ := dns.NewRR(`bosun.org. 300 IN PTR chell.bosun.org`)
|
||||
r2, _ := dns.NewRR(`bosun.org. 300 IN PTR barney.bosun.org.`)
|
||||
r3, _ := dns.NewRR(`bosun.org. 300 IN PTR alex.bosun.org.`)
|
||||
buf := &bytes.Buffer{}
|
||||
WriteZoneFileRR(buf, []dns.RR{r1, r2, r3}, "bosun.org", 99)
|
||||
if buf.String() != testdataZFPTR {
|
||||
t.Log(buf.String())
|
||||
t.Log(testdataZFPTR)
|
||||
t.Fatalf("Zone file does not match.")
|
||||
}
|
||||
parseAndRegen(t, buf, testdataZFPTR)
|
||||
}
|
||||
|
||||
var testdataZFPTR = `$TTL 300
|
||||
@ IN PTR alex.bosun.org.
|
||||
IN PTR barney.bosun.org.
|
||||
IN PTR chell.bosun.org.
|
||||
`
|
||||
|
||||
func TestWriteZoneFileCaa(t *testing.T) {
|
||||
// exhibits explicit ttls and long name
|
||||
r1, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 issuewild ";"`)
|
||||
r2, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 issue "letsencrypt.org"`)
|
||||
r3, _ := dns.NewRR(`bosun.org. 300 IN CAA 1 iodef "http://example.com"`)
|
||||
r4, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 iodef "https://example.com"`)
|
||||
r5, _ := dns.NewRR(`bosun.org. 300 IN CAA 0 iodef "https://example.net"`)
|
||||
r6, _ := dns.NewRR(`bosun.org. 300 IN CAA 1 iodef "mailto:example.com"`)
|
||||
buf := &bytes.Buffer{}
|
||||
WriteZoneFileRR(buf, []dns.RR{r1, r2, r3, r4, r5, r6}, "bosun.org", 99)
|
||||
if buf.String() != testdataZFCAA {
|
||||
t.Log(buf.String())
|
||||
t.Log(testdataZFCAA)
|
||||
t.Fatalf("Zone file does not match.")
|
||||
}
|
||||
parseAndRegen(t, buf, testdataZFCAA)
|
||||
}
|
||||
|
||||
var testdataZFCAA = `$TTL 300
|
||||
@ IN CAA 1 iodef "http://example.com"
|
||||
IN CAA 1 iodef "mailto:example.com"
|
||||
IN CAA 0 iodef "https://example.com"
|
||||
IN CAA 0 iodef "https://example.net"
|
||||
IN CAA 0 issue "letsencrypt.org"
|
||||
IN CAA 0 issuewild ";"
|
||||
`
|
||||
|
||||
// Test 1 of each record type
|
||||
|
||||
func mustNewRR(s string) dns.RR {
|
||||
r, err := dns.NewRR(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func TestWriteZoneFileEach(t *testing.T) {
|
||||
// Each rtype should be listed in this test exactly once.
|
||||
// If an rtype has more than one variations, add a test like TestWriteZoneFileCaa to test each.
|
||||
var d []dns.RR
|
||||
// #rtype_variations
|
||||
d = append(d, mustNewRR(`4.5 300 IN PTR y.bosun.org.`)) // Wouldn't actually be in this domain.
|
||||
d = append(d, mustNewRR(`bosun.org. 300 IN A 1.2.3.4`))
|
||||
d = append(d, mustNewRR(`bosun.org. 300 IN MX 1 bosun.org.`))
|
||||
d = append(d, mustNewRR(`bosun.org. 300 IN TXT "my text"`))
|
||||
d = append(d, mustNewRR(`bosun.org. 300 IN AAAA 4500:fe::1`))
|
||||
d = append(d, mustNewRR(`bosun.org. 300 IN SRV 10 10 9999 foo.com.`))
|
||||
d = append(d, mustNewRR(`bosun.org. 300 IN CAA 0 issue "letsencrypt.org"`))
|
||||
d = append(d, mustNewRR(`_443._tcp.bosun.org. 300 IN TLSA 3 1 1 abcdef0`)) // Label must be _port._proto
|
||||
d = append(d, mustNewRR(`sub.bosun.org. 300 IN NS bosun.org.`)) // Must be a label with no other records.
|
||||
d = append(d, mustNewRR(`x.bosun.org. 300 IN CNAME bosun.org.`)) // Must be a label with no other records.
|
||||
buf := &bytes.Buffer{}
|
||||
WriteZoneFileRR(buf, d, "bosun.org", 99)
|
||||
if buf.String() != testdataZFEach {
|
||||
t.Log(buf.String())
|
||||
t.Log(testdataZFEach)
|
||||
t.Fatalf("Zone file does not match.")
|
||||
}
|
||||
parseAndRegen(t, buf, testdataZFEach)
|
||||
}
|
||||
|
||||
var testdataZFEach = `$TTL 300
|
||||
@ IN A 1.2.3.4
|
||||
IN AAAA 4500:fe::1
|
||||
IN MX 1 bosun.org.
|
||||
IN SRV 10 10 9999 foo.com.
|
||||
IN TXT "my text"
|
||||
IN CAA 0 issue "letsencrypt.org"
|
||||
4.5 IN PTR y.bosun.org.
|
||||
_443._tcp IN TLSA 3 1 1 abcdef0
|
||||
sub IN NS bosun.org.
|
||||
x IN CNAME bosun.org.
|
||||
`
|
||||
|
||||
func TestWriteZoneFileSynth(t *testing.T) {
|
||||
r1, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.153")
|
||||
r2, _ := dns.NewRR("bosun.org. 300 IN A 192.30.252.154")
|
||||
r3, _ := dns.NewRR("www.bosun.org. 300 IN CNAME bosun.org.")
|
||||
rsynm := &models.RecordConfig{Type: "R53_ALIAS", TTL: 300}
|
||||
rsynm.SetLabel("myalias", "bosun.org")
|
||||
rsynz := &models.RecordConfig{Type: "R53_ALIAS", TTL: 300}
|
||||
rsynz.SetLabel("zalias", "bosun.org")
|
||||
|
||||
recs := models.RRstoRCs([]dns.RR{r1, r2, r3}, "bosun.org", 99)
|
||||
recs = append(recs, rsynm)
|
||||
recs = append(recs, rsynm)
|
||||
recs = append(recs, rsynz)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
WriteZoneFileRC(buf, recs, "bosun.org")
|
||||
expected := `$TTL 300
|
||||
@ IN A 192.30.252.153
|
||||
IN A 192.30.252.154
|
||||
;myalias IN R53_ALIAS type= zone_id=
|
||||
;myalias IN R53_ALIAS type= zone_id=
|
||||
www IN CNAME bosun.org.
|
||||
;zalias IN R53_ALIAS type= zone_id=
|
||||
`
|
||||
if buf.String() != expected {
|
||||
t.Log(buf.String())
|
||||
t.Log(expected)
|
||||
t.Fatalf("Zone file does not match.")
|
||||
}
|
||||
}
|
||||
|
||||
// Test sorting
|
||||
|
||||
func TestWriteZoneFileOrder(t *testing.T) {
|
||||
var records []dns.RR
|
||||
for i, td := range []string{
|
||||
"@",
|
||||
"@",
|
||||
"@",
|
||||
"stackoverflow.com.",
|
||||
"*",
|
||||
"foo",
|
||||
"bar.foo",
|
||||
"hip.foo",
|
||||
"mup",
|
||||
"a.mup",
|
||||
"bzt.mup",
|
||||
"aaa.bzt.mup",
|
||||
"zzz.bzt.mup",
|
||||
"nnn.mup",
|
||||
"zt.mup",
|
||||
"zap",
|
||||
} {
|
||||
name := dnsutil.AddOrigin(td, "stackoverflow.com.")
|
||||
r, _ := dns.NewRR(fmt.Sprintf("%s 300 IN A 1.2.3.%d", name, i))
|
||||
records = append(records, r)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
WriteZoneFileRR(buf, records, "stackoverflow.com", 99)
|
||||
// Compare
|
||||
if buf.String() != testdataOrder {
|
||||
t.Log("Found:")
|
||||
t.Log(buf.String())
|
||||
t.Log("Expected:")
|
||||
t.Log(testdataOrder)
|
||||
t.Fatalf("Zone file does not match.")
|
||||
}
|
||||
parseAndRegen(t, buf, testdataOrder)
|
||||
|
||||
// Now shuffle the list many times and make sure it still works:
|
||||
for iteration := 5; iteration > 0; iteration-- {
|
||||
// Randomize the list:
|
||||
perm := rand.Perm(len(records))
|
||||
for i, v := range perm {
|
||||
records[i], records[v] = records[v], records[i]
|
||||
}
|
||||
// Generate
|
||||
buf := &bytes.Buffer{}
|
||||
WriteZoneFileRR(buf, records, "stackoverflow.com", 99)
|
||||
// Compare
|
||||
if buf.String() != testdataOrder {
|
||||
t.Log(buf.String())
|
||||
t.Log(testdataOrder)
|
||||
t.Fatalf("Zone file does not match.")
|
||||
}
|
||||
parseAndRegen(t, buf, testdataOrder)
|
||||
}
|
||||
}
|
||||
|
||||
var testdataOrder = `$TTL 300
|
||||
@ IN A 1.2.3.0
|
||||
IN A 1.2.3.1
|
||||
IN A 1.2.3.2
|
||||
IN A 1.2.3.3
|
||||
* IN A 1.2.3.4
|
||||
foo IN A 1.2.3.5
|
||||
bar.foo IN A 1.2.3.6
|
||||
hip.foo IN A 1.2.3.7
|
||||
mup IN A 1.2.3.8
|
||||
a.mup IN A 1.2.3.9
|
||||
bzt.mup IN A 1.2.3.10
|
||||
aaa.bzt.mup IN A 1.2.3.11
|
||||
zzz.bzt.mup IN A 1.2.3.12
|
||||
nnn.mup IN A 1.2.3.13
|
||||
zt.mup IN A 1.2.3.14
|
||||
zap IN A 1.2.3.15
|
||||
`
|
||||
|
||||
// func formatLine
|
||||
|
||||
func TestFormatLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
lengths []int
|
||||
fields []string
|
||||
expected string
|
||||
}{
|
||||
{[]int{2, 2, 0}, []string{"a", "b", "c"}, "a b c"},
|
||||
{[]int{2, 2, 0}, []string{"aaaaa", "b", "c"}, "aaaaa b c"},
|
||||
}
|
||||
for _, ts := range tests {
|
||||
actual := formatLine(ts.lengths, ts.fields)
|
||||
if actual != ts.expected {
|
||||
t.Errorf("\"%s\" != \"%s\"", actual, ts.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// func zoneLabelLess
|
||||
|
||||
func TestZoneLabelLess(t *testing.T) {
|
||||
/*
|
||||
The zone should sort in prefix traversal order:
|
||||
|
||||
@
|
||||
*
|
||||
foo
|
||||
bar.foo
|
||||
hip.foo
|
||||
mup
|
||||
a.mup
|
||||
bzt.mup
|
||||
*.bzt.mup
|
||||
1.bzt.mup
|
||||
2.bzt.mup
|
||||
10.bzt.mup
|
||||
aaa.bzt.mup
|
||||
zzz.bzt.mup
|
||||
nnn.mup
|
||||
zt.mup
|
||||
zap
|
||||
*/
|
||||
|
||||
var tests = []struct {
|
||||
e1, e2 string
|
||||
expected bool
|
||||
}{
|
||||
{"@", "@", false},
|
||||
{"@", "*", true},
|
||||
{"@", "b", true},
|
||||
{"*", "@", false},
|
||||
{"*", "*", false},
|
||||
{"*", "b", true},
|
||||
{"foo", "foo", false},
|
||||
{"foo", "bar", false},
|
||||
{"bar", "foo", true},
|
||||
{"a.mup", "mup", false},
|
||||
{"mup", "a.mup", true},
|
||||
{"a.mup", "a.mup", false},
|
||||
{"a.mup", "bzt.mup", true},
|
||||
{"a.mup", "aa.mup", true},
|
||||
{"zt.mup", "aaa.bzt.mup", false},
|
||||
{"aaa.bzt.mup", "mup", false},
|
||||
{"*.bzt.mup", "aaa.bzt.mup", true},
|
||||
{"1.bzt.mup", "aaa.bzt.mup", true},
|
||||
{"1.bzt.mup", "2.bzt.mup", true},
|
||||
{"10.bzt.mup", "2.bzt.mup", false},
|
||||
{"nnn.mup", "aaa.bzt.mup", false},
|
||||
{`www\.miek.nl`, `www.miek.nl`, false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
actual := zoneLabelLess(test.e1, test.e2)
|
||||
if test.expected != actual {
|
||||
t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
|
||||
}
|
||||
actual = zoneLabelLess(test.e2, test.e1)
|
||||
// The reverse should work too:
|
||||
var expected bool
|
||||
if test.e1 == test.e2 {
|
||||
expected = false
|
||||
} else {
|
||||
expected = !test.expected
|
||||
}
|
||||
if expected != actual {
|
||||
t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestZoneRrtypeLess(t *testing.T) {
|
||||
/*
|
||||
In zonefiles we want to list SOAs, then NSs, then all others.
|
||||
*/
|
||||
|
||||
var tests = []struct {
|
||||
e1, e2 string
|
||||
expected bool
|
||||
}{
|
||||
{"SOA", "SOA", false},
|
||||
{"SOA", "A", true},
|
||||
{"SOA", "TXT", true},
|
||||
{"SOA", "NS", true},
|
||||
{"NS", "SOA", false},
|
||||
{"NS", "A", true},
|
||||
{"NS", "TXT", true},
|
||||
{"NS", "NS", false},
|
||||
{"A", "SOA", false},
|
||||
{"A", "A", false},
|
||||
{"A", "TXT", true},
|
||||
{"A", "NS", false},
|
||||
{"MX", "SOA", false},
|
||||
{"MX", "A", false},
|
||||
{"MX", "TXT", true},
|
||||
{"MX", "NS", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
actual := zoneRrtypeLess(test.e1, test.e2)
|
||||
if test.expected != actual {
|
||||
t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
|
||||
}
|
||||
actual = zoneRrtypeLess(test.e2, test.e1)
|
||||
// The reverse should work too:
|
||||
var expected bool
|
||||
if test.e1 == test.e2 {
|
||||
expected = false
|
||||
} else {
|
||||
expected = !test.expected
|
||||
}
|
||||
if expected != actual {
|
||||
t.Errorf("%v: expected (%v) got (%v)\n", test.e1, test.e2, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
194
pkg/prettyzone/sorting.go
Normal file
194
pkg/prettyzone/sorting.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package prettyzone
|
||||
|
||||
// Generate zonefiles.
|
||||
// This generates a zonefile that prioritizes beauty over efficiency.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v2/models"
|
||||
)
|
||||
|
||||
type zoneGenData struct {
|
||||
Origin string
|
||||
DefaultTTL uint32
|
||||
Records models.Records
|
||||
}
|
||||
|
||||
func (z *zoneGenData) Len() int { return len(z.Records) }
|
||||
func (z *zoneGenData) Swap(i, j int) { z.Records[i], z.Records[j] = z.Records[j], z.Records[i] }
|
||||
func (z *zoneGenData) Less(i, j int) bool {
|
||||
a, b := z.Records[i], z.Records[j]
|
||||
|
||||
// Sort by name.
|
||||
compA, compB := a.NameFQDN, b.NameFQDN
|
||||
if compA != compB {
|
||||
if a.Name == "@" {
|
||||
compA = "@"
|
||||
}
|
||||
if b.Name == "@" {
|
||||
compB = "@"
|
||||
}
|
||||
return zoneLabelLess(compA, compB)
|
||||
}
|
||||
|
||||
// sub-sort by type
|
||||
if a.Type != b.Type {
|
||||
return zoneRrtypeLess(a.Type, b.Type)
|
||||
}
|
||||
|
||||
// sub-sort within type:
|
||||
switch a.Type { // #rtype_variations
|
||||
case "A":
|
||||
ta2, tb2 := a.GetTargetIP(), b.GetTargetIP()
|
||||
ipa, ipb := ta2.To4(), tb2.To4()
|
||||
if ipa == nil || ipb == nil {
|
||||
log.Fatalf("should not happen: IPs are not 4 bytes: %#v %#v", ta2, tb2)
|
||||
}
|
||||
return bytes.Compare(ipa, ipb) == -1
|
||||
case "AAAA":
|
||||
ta2, tb2 := a.GetTargetIP(), b.GetTargetIP()
|
||||
ipa, ipb := ta2.To16(), tb2.To16()
|
||||
if ipa == nil || ipb == nil {
|
||||
log.Fatalf("should not happen: IPs are not 16 bytes: %#v %#v", ta2, tb2)
|
||||
}
|
||||
return bytes.Compare(ipa, ipb) == -1
|
||||
case "MX":
|
||||
// sort by priority. If they are equal, sort by Mx.
|
||||
if a.MxPreference == b.MxPreference {
|
||||
return a.GetTargetField() < b.GetTargetField()
|
||||
}
|
||||
return a.MxPreference < b.MxPreference
|
||||
case "SRV":
|
||||
//ta2, tb2 := a.(*dns.SRV), b.(*dns.SRV)
|
||||
pa, pb := a.SrvPort, b.SrvPort
|
||||
if pa != pb {
|
||||
return pa < pb
|
||||
}
|
||||
pa, pb = a.SrvPriority, b.SrvPriority
|
||||
if pa != pb {
|
||||
return pa < pb
|
||||
}
|
||||
pa, pb = a.SrvWeight, b.SrvWeight
|
||||
if pa != pb {
|
||||
return pa < pb
|
||||
}
|
||||
case "PTR":
|
||||
//ta2, tb2 := a.(*dns.PTR), b.(*dns.PTR)
|
||||
pa, pb := a.GetTargetField(), b.GetTargetField()
|
||||
if pa != pb {
|
||||
return pa < pb
|
||||
}
|
||||
case "CAA":
|
||||
//ta2, tb2 := a.(*dns.CAA), b.(*dns.CAA)
|
||||
// sort by tag
|
||||
pa, pb := a.CaaTag, b.CaaTag
|
||||
if pa != pb {
|
||||
return pa < pb
|
||||
}
|
||||
// then flag
|
||||
fa, fb := a.CaaFlag, b.CaaFlag
|
||||
if fa != fb {
|
||||
// flag set goes before ones without flag set
|
||||
return fa > fb
|
||||
}
|
||||
default:
|
||||
// pass through. String comparison is sufficient.
|
||||
}
|
||||
return a.String() < b.String()
|
||||
}
|
||||
|
||||
func zoneLabelLess(a, b string) bool {
|
||||
// Compare two zone labels for the purpose of sorting the RRs in a Zone.
|
||||
|
||||
// If they are equal, we are done. All other code is simplified
|
||||
// because we can assume a!=b.
|
||||
if a == b {
|
||||
return false
|
||||
}
|
||||
|
||||
// Sort @ at the top, then *, then everything else lexigraphically.
|
||||
// i.e. @ always is less. * is is less than everything but @.
|
||||
if a == "@" {
|
||||
return true
|
||||
}
|
||||
if b == "@" {
|
||||
return false
|
||||
}
|
||||
if a == "*" {
|
||||
return true
|
||||
}
|
||||
if b == "*" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Split into elements and match up last elements to first. Compare the
|
||||
// first non-equal elements.
|
||||
|
||||
as := strings.Split(a, ".")
|
||||
bs := strings.Split(b, ".")
|
||||
ia := len(as) - 1
|
||||
ib := len(bs) - 1
|
||||
|
||||
var min int
|
||||
if ia < ib {
|
||||
min = len(as) - 1
|
||||
} else {
|
||||
min = len(bs) - 1
|
||||
}
|
||||
|
||||
// Skip the matching highest elements, then compare the next item.
|
||||
for i, j := ia, ib; min >= 0; i, j, min = i-1, j-1, min-1 {
|
||||
// Compare as[i] < bs[j]
|
||||
// Sort @ at the top, then *, then everything else.
|
||||
// i.e. @ always is less. * is is less than everything but @.
|
||||
// If both are numeric, compare as integers, otherwise as strings.
|
||||
|
||||
if as[i] != bs[j] {
|
||||
|
||||
// If the first element is *, it is always less.
|
||||
if i == 0 && as[i] == "*" {
|
||||
return true
|
||||
}
|
||||
if j == 0 && bs[j] == "*" {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the elements are both numeric, compare as integers:
|
||||
au, aerr := strconv.ParseUint(as[i], 10, 64)
|
||||
bu, berr := strconv.ParseUint(bs[j], 10, 64)
|
||||
if aerr == nil && berr == nil {
|
||||
return au < bu
|
||||
}
|
||||
// otherwise, compare as strings:
|
||||
return as[i] < bs[j]
|
||||
}
|
||||
}
|
||||
// The min top elements were equal, so the shorter name is less.
|
||||
return ia < ib
|
||||
}
|
||||
|
||||
func zoneRrtypeLess(a, b string) bool {
|
||||
// Compare two RR types for the purpose of sorting the RRs in a Zone.
|
||||
|
||||
if a == b {
|
||||
return false
|
||||
}
|
||||
|
||||
// List SOAs, NSs, etc. then all others alphabetically.
|
||||
|
||||
for _, t := range []string{"SOA", "NS", "CNAME",
|
||||
"A", "AAAA", "MX", "SRV", "TXT",
|
||||
} {
|
||||
if a == t {
|
||||
return true
|
||||
}
|
||||
if b == t {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return a < b
|
||||
}
|
||||
Reference in New Issue
Block a user