mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-05-11 05:55:12 +00:00
Refactor RecordConfig: Add getters/setters (#314)
* Replace RecordConfig.Name and .NameFQDN with getters and setters. * Replace RecordConfig.Target with getters and setters. * Eliminate the CombinedTarget concept. * Add RecordConfig.PopulateFromString to reduce code in all providers. * encode and decode name.com txt records (#315) * Replace fmt.Errorf with errors.Errorf
This commit is contained in:
277
models/record.go
Normal file
277
models/record.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// RecordConfig stores a DNS record.
|
||||
// Valid types:
|
||||
// Official:
|
||||
// A
|
||||
// AAAA
|
||||
// ANAME // Technically not an official rtype yet.
|
||||
// CAA
|
||||
// CNAME
|
||||
// MX
|
||||
// NS
|
||||
// PTR
|
||||
// SRV
|
||||
// TLSA
|
||||
// TXT
|
||||
// Pseudo-Types:
|
||||
// ALIAs
|
||||
// CF_REDIRECT
|
||||
// CF_TEMP_REDIRECT
|
||||
// FRAME
|
||||
// IMPORT_TRANSFORM
|
||||
// NAMESERVER
|
||||
// NO_PURGE
|
||||
// PAGE_RULE
|
||||
// PURGE
|
||||
// URL
|
||||
// URL301
|
||||
//
|
||||
// Notes about the fields:
|
||||
//
|
||||
// Name:
|
||||
// This is the shortname i.e. the NameFQDN without the origin suffix.
|
||||
// It should never have a trailing "."
|
||||
// It should never be null. The apex (naked domain) is stored as "@".
|
||||
// If the origin is "foo.com." and Name is "foo.com", this literally means
|
||||
// the intended FQDN is "foo.com.foo.com." (which may look odd)
|
||||
// NameFQDN:
|
||||
// This is the FQDN version of Name.
|
||||
// It should never have a trailiing ".".
|
||||
// NOTE: Eventually we will unexport Name/NameFQDN. Please start using
|
||||
// the setters (SetLabel/SetLabelFromFQDN) and getters (GetLabel/GetLabelFQDN).
|
||||
// as they will always work.
|
||||
// Target:
|
||||
// This is the host or IP address of the record, with
|
||||
// the other related paramters (weight, priority, etc.) stored in individual
|
||||
// fields.
|
||||
// NOTE: Eventually we will unexport Target. Please start using the
|
||||
// setters (SetTarget*) and getters (GetTarget*) as they will always work.
|
||||
//
|
||||
// Idioms:
|
||||
// rec.Label() == "@" // Is this record at the apex?
|
||||
//
|
||||
type RecordConfig struct {
|
||||
Type string `json:"type"` // All caps rtype name.
|
||||
Name string `json:"name"` // The short name. See above.
|
||||
NameFQDN string `json:"-"` // Must end with ".$origin". See above.
|
||||
Target string `json:"target"` // If a name, must end with "."
|
||||
TTL uint32 `json:"ttl,omitempty"`
|
||||
Metadata map[string]string `json:"meta,omitempty"`
|
||||
MxPreference uint16 `json:"mxpreference,omitempty"`
|
||||
SrvPriority uint16 `json:"srvpriority,omitempty"`
|
||||
SrvWeight uint16 `json:"srvweight,omitempty"`
|
||||
SrvPort uint16 `json:"srvport,omitempty"`
|
||||
CaaTag string `json:"caatag,omitempty"`
|
||||
CaaFlag uint8 `json:"caaflag,omitempty"`
|
||||
TlsaUsage uint8 `json:"tlsausage,omitempty"`
|
||||
TlsaSelector uint8 `json:"tlsaselector,omitempty"`
|
||||
TlsaMatchingType uint8 `json:"tlsamatchingtype,omitempty"`
|
||||
TxtStrings []string `json:"txtstrings,omitempty"` // TxtStrings stores all strings (including the first). Target stores only the first one.
|
||||
R53Alias map[string]string `json:"r53_alias,omitempty"`
|
||||
|
||||
Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing.
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of a RecordConfig.
|
||||
func (rc *RecordConfig) Copy() (*RecordConfig, error) {
|
||||
newR := &RecordConfig{}
|
||||
err := copyObj(rc, newR)
|
||||
return newR, err
|
||||
}
|
||||
|
||||
// SetLabel sets the .Name/.NameFQDN fields given a short name and origin.
|
||||
// origin must not have a trailing dot: The entire code base
|
||||
// maintains dc.Name without the trailig dot. Finding a dot here means
|
||||
// something is very wrong.
|
||||
// short must not have a training dot: That would mean you have
|
||||
// a FQDN, and shouldn't be using SetLabel(). Maybe SetLabelFromFQDN()?
|
||||
func (rc *RecordConfig) SetLabel(short, origin string) {
|
||||
|
||||
// Assertions that make sure the function is being used correctly:
|
||||
if strings.HasSuffix(origin, ".") {
|
||||
panic(errors.Errorf("origin (%s) is not supposed to end with a dot", origin))
|
||||
}
|
||||
if strings.HasSuffix(short, ".") {
|
||||
panic(errors.Errorf("short (%s) is not supposed to end with a dot", origin))
|
||||
}
|
||||
|
||||
// TODO(tlim): We should add more validation here or in a separate validation
|
||||
// module. We might want to check things like (\w+\.)+
|
||||
|
||||
short = strings.ToLower(short)
|
||||
origin = strings.ToLower(origin)
|
||||
if short == "" || short == "@" {
|
||||
rc.Name = "@"
|
||||
rc.NameFQDN = origin
|
||||
} else {
|
||||
rc.Name = short
|
||||
rc.NameFQDN = dnsutil.AddOrigin(short, origin)
|
||||
}
|
||||
}
|
||||
|
||||
// SetLabelFromFQDN sets the .Name/.NameFQDN fields given a FQDN and origin.
|
||||
// fqdn may have a trailing "." but it is not required.
|
||||
// origin may not have a trailing dot.
|
||||
func (rc *RecordConfig) SetLabelFromFQDN(fqdn, origin string) {
|
||||
|
||||
// Assertions that make sure the function is being used correctly:
|
||||
if strings.HasSuffix(origin, ".") {
|
||||
panic(errors.Errorf("origin (%s) is not supposed to end with a dot", origin))
|
||||
}
|
||||
if strings.HasSuffix(fqdn, "..") {
|
||||
panic(errors.Errorf("fqdn (%s) is not supposed to end with double dots", origin))
|
||||
}
|
||||
|
||||
if strings.HasSuffix(fqdn, ".") {
|
||||
// Trim off a trailing dot.
|
||||
fqdn = fqdn[:len(fqdn)-1]
|
||||
}
|
||||
|
||||
fqdn = strings.ToLower(fqdn)
|
||||
origin = strings.ToLower(origin)
|
||||
rc.Name = dnsutil.TrimDomainName(fqdn, origin)
|
||||
rc.NameFQDN = fqdn
|
||||
}
|
||||
|
||||
// GetLabel returns the shortname of the label associated with this RecordConfig.
|
||||
// It will never end with "."
|
||||
// It does not need further shortening (i.e. if it returns "foo.com" and the
|
||||
// domain is "foo.com" then the FQDN is actually "foo.com.foo.com").
|
||||
// It will never be "" (the apex is returned as "@").
|
||||
func (rc *RecordConfig) GetLabel() string {
|
||||
return rc.Name
|
||||
}
|
||||
|
||||
// GetLabelFQDN returns the FQDN of the label associated with this RecordConfig.
|
||||
// It will not end with ".".
|
||||
func (rc *RecordConfig) GetLabelFQDN() string {
|
||||
return rc.NameFQDN
|
||||
}
|
||||
|
||||
// ToRR converts a RecordConfig to a dns.RR.
|
||||
func (rc *RecordConfig) ToRR() dns.RR {
|
||||
|
||||
// Don't call this on fake types.
|
||||
rdtype, ok := dns.StringToType[rc.Type]
|
||||
if !ok {
|
||||
log.Fatalf("No such DNS type as (%#v)\n", rc.Type)
|
||||
}
|
||||
|
||||
// Magicallly create an RR of the correct type.
|
||||
rr := dns.TypeToRR[rdtype]()
|
||||
|
||||
// Fill in the header.
|
||||
rr.Header().Name = rc.NameFQDN + "."
|
||||
rr.Header().Rrtype = rdtype
|
||||
rr.Header().Class = dns.ClassINET
|
||||
rr.Header().Ttl = rc.TTL
|
||||
if rc.TTL == 0 {
|
||||
rr.Header().Ttl = DefaultTTL
|
||||
}
|
||||
|
||||
// Fill in the data.
|
||||
switch rdtype { // #rtype_variations
|
||||
case dns.TypeA:
|
||||
rr.(*dns.A).A = rc.GetTargetIP()
|
||||
case dns.TypeAAAA:
|
||||
rr.(*dns.AAAA).AAAA = rc.GetTargetIP()
|
||||
case dns.TypeCNAME:
|
||||
rr.(*dns.CNAME).Target = rc.GetTargetField()
|
||||
case dns.TypePTR:
|
||||
rr.(*dns.PTR).Ptr = rc.GetTargetField()
|
||||
case dns.TypeMX:
|
||||
rr.(*dns.MX).Preference = rc.MxPreference
|
||||
rr.(*dns.MX).Mx = rc.GetTargetField()
|
||||
case dns.TypeNS:
|
||||
rr.(*dns.NS).Ns = rc.GetTargetField()
|
||||
case dns.TypeSOA:
|
||||
t := strings.Replace(rc.GetTargetField(), `\ `, ` `, -1)
|
||||
parts := strings.Fields(t)
|
||||
rr.(*dns.SOA).Ns = parts[0]
|
||||
rr.(*dns.SOA).Mbox = parts[1]
|
||||
rr.(*dns.SOA).Serial = atou32(parts[2])
|
||||
rr.(*dns.SOA).Refresh = atou32(parts[3])
|
||||
rr.(*dns.SOA).Retry = atou32(parts[4])
|
||||
rr.(*dns.SOA).Expire = atou32(parts[5])
|
||||
rr.(*dns.SOA).Minttl = atou32(parts[6])
|
||||
case dns.TypeSRV:
|
||||
rr.(*dns.SRV).Priority = rc.SrvPriority
|
||||
rr.(*dns.SRV).Weight = rc.SrvWeight
|
||||
rr.(*dns.SRV).Port = rc.SrvPort
|
||||
rr.(*dns.SRV).Target = rc.GetTargetField()
|
||||
case dns.TypeCAA:
|
||||
rr.(*dns.CAA).Flag = rc.CaaFlag
|
||||
rr.(*dns.CAA).Tag = rc.CaaTag
|
||||
rr.(*dns.CAA).Value = rc.GetTargetField()
|
||||
case dns.TypeTLSA:
|
||||
rr.(*dns.TLSA).Usage = rc.TlsaUsage
|
||||
rr.(*dns.TLSA).MatchingType = rc.TlsaMatchingType
|
||||
rr.(*dns.TLSA).Selector = rc.TlsaSelector
|
||||
rr.(*dns.TLSA).Certificate = rc.GetTargetField()
|
||||
case dns.TypeTXT:
|
||||
rr.(*dns.TXT).Txt = rc.TxtStrings
|
||||
default:
|
||||
panic(fmt.Sprintf("ToRR: Unimplemented rtype %v", rc.Type))
|
||||
// We panic so that we quickly find any switch statements
|
||||
// that have not been updated for a new RR type.
|
||||
}
|
||||
|
||||
return rr
|
||||
}
|
||||
|
||||
// RecordKey represents a resource record in a format used by some systems.
|
||||
type RecordKey struct {
|
||||
Name string
|
||||
Type string
|
||||
}
|
||||
|
||||
// Key converts a RecordConfig into a RecordKey.
|
||||
func (rc *RecordConfig) Key() RecordKey {
|
||||
return RecordKey{rc.Name, rc.Type}
|
||||
}
|
||||
|
||||
// Records is a list of *RecordConfig.
|
||||
type Records []*RecordConfig
|
||||
|
||||
// Grouped returns a map of keys to records.
|
||||
func (r Records) Grouped() map[RecordKey]Records {
|
||||
groups := map[RecordKey]Records{}
|
||||
for _, rec := range r {
|
||||
groups[rec.Key()] = append(groups[rec.Key()], rec)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// PostProcessRecords does any post-processing of the downloaded DNS records.
|
||||
func PostProcessRecords(recs []*RecordConfig) {
|
||||
Downcase(recs)
|
||||
//fixTxt(recs)
|
||||
}
|
||||
|
||||
// Downcase converts all labels and targets to lowercase in a list of RecordConfig.
|
||||
func Downcase(recs []*RecordConfig) {
|
||||
for _, r := range recs {
|
||||
r.Name = strings.ToLower(r.Name)
|
||||
r.NameFQDN = strings.ToLower(r.NameFQDN)
|
||||
switch r.Type {
|
||||
case "ANAME", "CNAME", "MX", "NS", "PTR":
|
||||
r.Target = strings.ToLower(r.Target)
|
||||
case "A", "AAAA", "ALIAS", "CAA", "IMPORT_TRANSFORM", "SRV", "TLSA", "TXT", "SOA", "CF_REDIRECT", "CF_TEMP_REDIRECT":
|
||||
// Do nothing.
|
||||
default:
|
||||
// TODO: we'd like to panic here, but custom record types complicate things.
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
Reference in New Issue
Block a user