github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/pkg/prettyzone/prettyzone.go (about)

     1  package prettyzone
     2  
     3  // Generate zonefiles.
     4  // This generates a zonefile that prioritizes beauty over efficiency.
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/StackExchange/dnscontrol/v2/models"
    13  	"github.com/miekg/dns"
    14  )
    15  
    16  // MostCommonTTL returns the most common TTL in a set of records. If there is
    17  // a tie, the highest TTL is selected. This makes the results consistent.
    18  // NS records are not included in the analysis because Tom said so.
    19  func MostCommonTTL(records models.Records) uint32 {
    20  	// Index the TTLs in use:
    21  	d := make(map[uint32]int)
    22  	for _, r := range records {
    23  		if r.Type != "NS" {
    24  			d[r.TTL]++
    25  		}
    26  	}
    27  	// Find the largest count:
    28  	var mc int
    29  	for _, value := range d {
    30  		if value > mc {
    31  			mc = value
    32  		}
    33  	}
    34  	// Find the largest key with that count:
    35  	var mk uint32
    36  	for key, value := range d {
    37  		if value == mc {
    38  			if key > mk {
    39  				mk = key
    40  			}
    41  		}
    42  	}
    43  	return mk
    44  }
    45  
    46  // WriteZoneFileRR is a helper for when you have []dns.RR instead of models.Records
    47  func WriteZoneFileRR(w io.Writer, records []dns.RR, origin string) error {
    48  	return WriteZoneFileRC(w, models.RRstoRCs(records, origin), origin, 0, nil)
    49  }
    50  
    51  // WriteZoneFileRC writes a beautifully formatted zone file.
    52  func WriteZoneFileRC(w io.Writer, records models.Records, origin string, defaultTTL uint32, comments []string) error {
    53  	// This function prioritizes beauty over output size.
    54  	// * The zone records are sorted by label, grouped by subzones to
    55  	//   be easy to read and pleasant to the eye.
    56  	// * Within a label, SOA and NS records are listed first.
    57  	// * MX records are sorted numericly by preference value.
    58  	// * SRV records are sorted numericly by port, then priority, then weight.
    59  	// * A records are sorted by IP address, not lexicographically.
    60  	// * Repeated labels are removed.
    61  	// * $TTL is used to eliminate clutter. The most common TTL value is used.
    62  	// * "@" is used instead of the apex domain name.
    63  
    64  	if defaultTTL == 0 {
    65  		defaultTTL = MostCommonTTL(records)
    66  	}
    67  
    68  	z := PrettySort(records, origin, defaultTTL, comments)
    69  
    70  	return z.generateZoneFileHelper(w)
    71  }
    72  
    73  func PrettySort(records models.Records, origin string, defaultTTL uint32, comments []string) *zoneGenData {
    74  	if defaultTTL == 0 {
    75  		defaultTTL = MostCommonTTL(records)
    76  	}
    77  	z := &zoneGenData{
    78  		Origin:     origin + ".",
    79  		DefaultTTL: defaultTTL,
    80  		Comments:   comments,
    81  	}
    82  	if z.DefaultTTL == 0 {
    83  		z.DefaultTTL = 300
    84  	}
    85  	z.Records = nil
    86  	for _, r := range records {
    87  		z.Records = append(z.Records, r)
    88  	}
    89  	return z
    90  }
    91  
    92  // generateZoneFileHelper creates a pretty zonefile.
    93  func (z *zoneGenData) generateZoneFileHelper(w io.Writer) error {
    94  
    95  	nameShortPrevious := ""
    96  
    97  	sort.Sort(z)
    98  	if z.DefaultTTL == 0 {
    99  		z.DefaultTTL = 300
   100  	}
   101  	fmt.Fprintln(w, "$TTL", z.DefaultTTL)
   102  	for _, comment := range z.Comments {
   103  		for _, line := range strings.Split(comment, "\n") {
   104  			if line != "" {
   105  				fmt.Fprintln(w, ";", line)
   106  			}
   107  		}
   108  	}
   109  	for i, rr := range z.Records {
   110  
   111  		// Fake types are commented out.
   112  		prefix := ""
   113  		_, ok := dns.StringToType[rr.Type]
   114  		if !ok {
   115  			prefix = ";"
   116  		}
   117  
   118  		// name
   119  		nameShort := rr.Name
   120  		name := nameShort
   121  		if (prefix == "") && (i > 0 && nameShort == nameShortPrevious) {
   122  			name = ""
   123  		} else {
   124  			name = nameShort
   125  		}
   126  		nameShortPrevious = nameShort
   127  
   128  		// ttl
   129  		ttl := ""
   130  		if rr.TTL != z.DefaultTTL && rr.TTL != 0 {
   131  			ttl = fmt.Sprint(rr.TTL)
   132  		}
   133  
   134  		// type
   135  		typeStr := rr.Type
   136  
   137  		// the remaining line
   138  		target := rr.GetTargetCombined()
   139  
   140  		fmt.Fprintf(w, "%s%s\n",
   141  			prefix, formatLine([]int{10, 5, 2, 5, 0}, []string{name, ttl, "IN", typeStr, target}))
   142  	}
   143  	return nil
   144  }
   145  
   146  func formatLine(lengths []int, fields []string) string {
   147  	c := 0
   148  	result := ""
   149  	for i, length := range lengths {
   150  		item := fields[i]
   151  		for len(result) < c {
   152  			result += " "
   153  		}
   154  		if item != "" {
   155  			result += item + " "
   156  		}
   157  		c += length + 1
   158  	}
   159  	return strings.TrimRight(result, " ")
   160  }