1
0
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:
Tom Limoncelli
2018-02-15 12:02:50 -05:00
committed by GitHub
parent 324b1ea930
commit de4455942b
37 changed files with 1237 additions and 965 deletions

View File

@@ -1,20 +1,7 @@
package models
import (
"bytes"
"encoding/gob"
"encoding/json"
"fmt"
"log"
"net"
"reflect"
"strconv"
"strings"
"github.com/StackExchange/dnscontrol/pkg/transform"
"github.com/miekg/dns"
"github.com/pkg/errors"
"golang.org/x/net/idna"
)
// DefaultTTL is applied to any DNS record without an explicit TTL.
@@ -53,287 +40,6 @@ type DNSProviderConfig struct {
Metadata json.RawMessage `json:"meta,omitempty"`
}
// RecordConfig stores a DNS record.
// Providers are responsible for validating or normalizing the data
// that goes into a RecordConfig.
// If you update Name, you have to update NameFQDN and vice-versa.
//
// Name:
// This is the shortname i.e. the NameFQDN without the origin suffix.
// It should never have a trailing "."
// It should never be null. It should store It "@", not the apex domain, not null, etc.
// It shouldn't end with the domain origin. If the origin is "foo.com." then
// if Name == "foo.com" then that literally means "foo.com.foo.com." is
// the intended FQDN.
// NameFQDN:
// This is the FQDN version of Name.
// It should never have a trailiing ".".
// Valid types:
// Official:
// A
// AAAA
// ANAME
// 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
type RecordConfig struct {
Type string `json:"type"`
Name string `json:"name"` // The short name. See below.
Target string `json:"target"` // If a name, must end with "."
TTL uint32 `json:"ttl,omitempty"`
Metadata map[string]string `json:"meta,omitempty"`
NameFQDN string `json:"-"` // Must end with ".$origin". See below.
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"`
CombinedTarget bool `json:"-"`
Original interface{} `json:"-"` // Store pointer to provider-specific record object. Used in diffing.
}
func (rc *RecordConfig) String() (content string) {
if rc.CombinedTarget {
return rc.Target
}
content = fmt.Sprintf("%s %s %s %d", rc.Type, rc.NameFQDN, rc.Target, rc.TTL)
switch rc.Type { // #rtype_variations
case "A", "AAAA", "CNAME", "NS", "PTR", "TXT":
// Nothing special.
case "MX":
content += fmt.Sprintf(" pref=%d", rc.MxPreference)
case "SOA":
content = fmt.Sprintf("%s %s %s %d", rc.Type, rc.Name, rc.Target, rc.TTL)
case "SRV":
content += fmt.Sprintf(" srvpriority=%d srvweight=%d srvport=%d", rc.SrvPriority, rc.SrvWeight, rc.SrvPort)
case "TLSA":
content += fmt.Sprintf(" tlsausage=%d tlsaselector=%d tlsamatchingtype=%d", rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType)
case "CAA":
content += fmt.Sprintf(" caatag=%s caaflag=%d", rc.CaaTag, rc.CaaFlag)
case "R53_ALIAS":
content += fmt.Sprintf(" type=%s zone_id=%s", rc.R53Alias["type"], rc.R53Alias["zone_id"])
default:
msg := fmt.Sprintf("rc.String rtype %v unimplemented", rc.Type)
panic(msg)
// We panic so that we quickly find any switch statements
// that have not been updated for a new RR type.
}
for k, v := range rc.Metadata {
content += fmt.Sprintf(" %s=%s", k, v)
}
return content
}
// Content combines Target and other fields into one string.
func (rc *RecordConfig) Content() string {
if rc.CombinedTarget {
return rc.Target
}
// If this is a pseudo record, just return the target.
if _, ok := dns.StringToType[rc.Type]; !ok {
return rc.Target
}
// We cheat by converting to a dns.RR and use the String() function.
// Sadly that function always includes a header, which we must strip out.
// TODO(tlim): Request the dns project add a function that returns
// the string without the header.
rr := rc.ToRR()
header := rr.Header().String()
full := rr.String()
if !strings.HasPrefix(full, header) {
panic("dns.Hdr.String() not acting as we expect")
}
return full[len(header):]
}
// MergeToTarget combines "extra" fields into .Target, and zeros the merged fields.
func (rc *RecordConfig) MergeToTarget() {
if rc.CombinedTarget {
pm := strings.Join([]string{"MergeToTarget: Already collapsed: ", rc.Name, rc.Target}, " ")
panic(pm)
}
// Merge "extra" fields into the Target.
rc.Target = rc.Content()
// Zap any fields that may have been merged.
rc.MxPreference = 0
rc.SrvPriority = 0
rc.SrvWeight = 0
rc.SrvPort = 0
rc.CaaFlag = 0
rc.CaaTag = ""
rc.TlsaUsage = 0
rc.TlsaMatchingType = 0
rc.TlsaSelector = 0
rc.CombinedTarget = true
}
// 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 = net.ParseIP(rc.Target)
case dns.TypeAAAA:
rr.(*dns.AAAA).AAAA = net.ParseIP(rc.Target)
case dns.TypeCNAME:
rr.(*dns.CNAME).Target = rc.Target
case dns.TypePTR:
rr.(*dns.PTR).Ptr = rc.Target
case dns.TypeMX:
rr.(*dns.MX).Preference = rc.MxPreference
rr.(*dns.MX).Mx = rc.Target
case dns.TypeNS:
rr.(*dns.NS).Ns = rc.Target
case dns.TypeSOA:
t := strings.Replace(rc.Target, `\ `, ` `, -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.Target
case dns.TypeCAA:
rr.(*dns.CAA).Flag = rc.CaaFlag
rr.(*dns.CAA).Tag = rc.CaaTag
rr.(*dns.CAA).Value = rc.Target
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.Target
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
}
func atou32(s string) uint32 {
i64, err := strconv.ParseInt(s, 10, 32)
if err != nil {
panic(fmt.Sprintf("atou32 failed (%v) (err=%v", s, err))
}
return uint32(i64)
}
// 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
}
// fixTxt fixes TXT records generated by providers that do not understand CanUseTXTMulti.
func fixTxt(recs []*RecordConfig) {
for _, r := range recs {
if r.Type == "TXT" {
if len(r.TxtStrings) == 0 {
r.TxtStrings = []string{r.Target}
}
}
}
}
// 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}
}
// Nameserver describes a nameserver.
type Nameserver struct {
Name string `json:"name"` // Normalized to a FQDN with NO trailing "."
@@ -349,252 +55,6 @@ func StringsToNameservers(nss []string) []*Nameserver {
return nservers
}
// DomainConfig describes a DNS domain (tecnically a DNS zone).
type DomainConfig struct {
Name string `json:"name"` // NO trailing "."
RegistrarName string `json:"registrar"`
DNSProviderNames map[string]int `json:"dnsProviders"`
Metadata map[string]string `json:"meta,omitempty"`
Records Records `json:"records"`
Nameservers []*Nameserver `json:"nameservers,omitempty"`
KeepUnknown bool `json:"keepunknown,omitempty"`
IgnoredLabels []string `json:"ignored_labels,omitempty"`
// These fields contain instantiated provider instances once everything is linked up.
// This linking is in two phases:
// 1. Metadata (name/type) is availible just from the dnsconfig. Validation can use that.
// 2. Final driver instances are loaded after we load credentials. Any actual provider interaction requires that.
RegistrarInstance *RegistrarInstance `json:"-"`
DNSProviderInstances []*DNSProviderInstance `json:"-"`
}
// Copy returns a deep copy of the DomainConfig.
func (dc *DomainConfig) Copy() (*DomainConfig, error) {
newDc := &DomainConfig{}
// provider instances are interfaces that gob hates if you don't register them.
// and the specific types are not gob encodable since nothing is exported.
// should find a better solution for this now.
//
// current strategy: remove everything, gob copy it. Then set both to stored copy.
reg := dc.RegistrarInstance
dnsps := dc.DNSProviderInstances
dc.RegistrarInstance = nil
dc.DNSProviderInstances = nil
err := copyObj(dc, newDc)
dc.RegistrarInstance = reg
newDc.RegistrarInstance = reg
dc.DNSProviderInstances = dnsps
newDc.DNSProviderInstances = dnsps
return newDc, err
}
// Copy returns a deep copy of a RecordConfig.
func (rc *RecordConfig) Copy() (*RecordConfig, error) {
newR := &RecordConfig{}
err := copyObj(rc, newR)
return newR, err
}
// Punycode will convert all records to punycode format.
// It will encode:
// - Name
// - NameFQDN
// - Target (CNAME and MX only)
func (dc *DomainConfig) Punycode() error {
var err error
for _, rec := range dc.Records {
rec.Name, err = idna.ToASCII(rec.Name)
if err != nil {
return err
}
rec.NameFQDN, err = idna.ToASCII(rec.NameFQDN)
if err != nil {
return err
}
switch rec.Type { // #rtype_variations
case "ALIAS", "MX", "NS", "CNAME", "PTR", "SRV", "URL", "URL301", "FRAME", "R53_ALIAS":
rec.Target, err = idna.ToASCII(rec.Target)
if err != nil {
return err
}
case "A", "AAAA", "CAA", "TXT", "TLSA":
// Nothing to do.
default:
msg := fmt.Sprintf("Punycode rtype %v unimplemented", rec.Type)
panic(msg)
// We panic so that we quickly find any switch statements
// that have not been updated for a new RR type.
}
}
return nil
}
// CombineMXs will merge the priority into the target field for all mx records.
// Useful for providers that desire them as one field.
func (dc *DomainConfig) CombineMXs() {
for _, rec := range dc.Records {
if rec.Type == "MX" {
if rec.CombinedTarget {
pm := strings.Join([]string{"CombineMXs: Already collapsed: ", rec.Name, rec.Target}, " ")
panic(pm)
}
rec.Target = fmt.Sprintf("%d %s", rec.MxPreference, rec.Target)
rec.MxPreference = 0
rec.CombinedTarget = true
}
}
}
// SplitCombinedMxValue splits a combined MX preference and target into
// separate entities, i.e. splitting "10 aspmx2.googlemail.com."
// into "10" and "aspmx2.googlemail.com.".
func SplitCombinedMxValue(s string) (preference uint16, target string, err error) {
parts := strings.Fields(s)
if len(parts) != 2 {
return 0, "", errors.Errorf("MX value %#v contains too many fields", s)
}
n64, err := strconv.ParseUint(parts[0], 10, 16)
if err != nil {
return 0, "", errors.Errorf("MX preference %#v does not fit into a uint16", parts[0])
}
return uint16(n64), parts[1], nil
}
// CombineSRVs will merge the priority, weight, and port into the target field for all srv records.
// Useful for providers that desire them as one field.
func (dc *DomainConfig) CombineSRVs() {
for _, rec := range dc.Records {
if rec.Type == "SRV" {
if rec.CombinedTarget {
pm := strings.Join([]string{"CombineSRVs: Already collapsed: ", rec.Name, rec.Target}, " ")
panic(pm)
}
rec.Target = fmt.Sprintf("%d %d %d %s", rec.SrvPriority, rec.SrvWeight, rec.SrvPort, rec.Target)
rec.CombinedTarget = true
}
}
}
// SplitCombinedSrvValue splits a combined SRV priority, weight, port and target into
// separate entities, some DNS providers want "5" "10" 15" and "foo.com.",
// while other providers want "5 10 15 foo.com.".
func SplitCombinedSrvValue(s string) (priority, weight, port uint16, target string, err error) {
parts := strings.Fields(s)
if len(parts) != 4 {
return 0, 0, 0, "", errors.Errorf("SRV value %#v contains too many fields", s)
}
priorityconv, err := strconv.ParseInt(parts[0], 10, 16)
if err != nil {
return 0, 0, 0, "", errors.Errorf("Priority %#v does not fit into a uint16", parts[0])
}
weightconv, err := strconv.ParseInt(parts[1], 10, 16)
if err != nil {
return 0, 0, 0, "", errors.Errorf("Weight %#v does not fit into a uint16", parts[0])
}
portconv, err := strconv.ParseInt(parts[2], 10, 16)
if err != nil {
return 0, 0, 0, "", errors.Errorf("Port %#v does not fit into a uint16", parts[0])
}
return uint16(priorityconv), uint16(weightconv), uint16(portconv), parts[3], nil
}
// CombineCAAs will merge the tags and flags into the target field for all CAA records.
// Useful for providers that desire them as one field.
func (dc *DomainConfig) CombineCAAs() {
for _, rec := range dc.Records {
if rec.Type == "CAA" {
if rec.CombinedTarget {
pm := strings.Join([]string{"CombineCAAs: Already collapsed: ", rec.Name, rec.Target}, " ")
panic(pm)
}
rec.Target = rec.Content()
rec.CombinedTarget = true
}
}
}
// SplitCombinedCaaValue parses a string listing the parts of a CAA record into its components.
func SplitCombinedCaaValue(s string) (tag string, flag uint8, value string, err error) {
splitData := strings.SplitN(s, " ", 3)
if len(splitData) != 3 {
err = errors.Errorf("Unexpected data for CAA record returned by Vultr")
return
}
lflag, err := strconv.ParseUint(splitData[0], 10, 8)
if err != nil {
return
}
flag = uint8(lflag)
tag = splitData[1]
value = splitData[2]
if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) {
value = value[1 : len(value)-1]
}
if strings.HasPrefix(value, `'`) && strings.HasSuffix(value, `'`) {
value = value[1 : len(value)-1]
}
return
}
func copyObj(input interface{}, output interface{}) error {
buf := &bytes.Buffer{}
enc := gob.NewEncoder(buf)
dec := gob.NewDecoder(buf)
if err := enc.Encode(input); err != nil {
return err
}
return dec.Decode(output)
}
// HasRecordTypeName returns True if there is a record with this rtype and name.
func (dc *DomainConfig) HasRecordTypeName(rtype, name string) bool {
for _, r := range dc.Records {
if r.Type == rtype && r.Name == name {
return true
}
}
return false
}
// Filter removes all records that don't match the filter f.
func (dc *DomainConfig) Filter(f func(r *RecordConfig) bool) {
recs := []*RecordConfig{}
for _, r := range dc.Records {
if f(r) {
recs = append(recs, r)
}
}
dc.Records = recs
}
// InterfaceToIP returns an IP address when given a 32-bit value or a string. That is,
// dnsconfig.js output may represent IP addresses as either a string ("1.2.3.4")
// or as an numeric value (the integer representation of the 32-bit value). This function
// converts either to a net.IP.
func InterfaceToIP(i interface{}) (net.IP, error) {
switch v := i.(type) {
case float64:
u := uint32(v)
return transform.UintToIP(u), nil
case string:
if ip := net.ParseIP(v); ip != nil {
return ip, nil
}
return nil, errors.Errorf("%s is not a valid ip address", v)
default:
return nil, errors.Errorf("cannot convert type %s to ip", reflect.TypeOf(i))
}
}
// Correction is anything that can be run. Implementation is up to the specific provider.
type Correction struct {
F func() error `json:"-"`

102
models/domain.go Normal file
View File

@@ -0,0 +1,102 @@
package models
import (
"fmt"
"golang.org/x/net/idna"
)
// DomainConfig describes a DNS domain (tecnically a DNS zone).
type DomainConfig struct {
Name string `json:"name"` // NO trailing "."
RegistrarName string `json:"registrar"`
DNSProviderNames map[string]int `json:"dnsProviders"`
Metadata map[string]string `json:"meta,omitempty"`
Records Records `json:"records"`
Nameservers []*Nameserver `json:"nameservers,omitempty"`
KeepUnknown bool `json:"keepunknown,omitempty"`
IgnoredLabels []string `json:"ignored_labels,omitempty"`
// These fields contain instantiated provider instances once everything is linked up.
// This linking is in two phases:
// 1. Metadata (name/type) is availible just from the dnsconfig. Validation can use that.
// 2. Final driver instances are loaded after we load credentials. Any actual provider interaction requires that.
RegistrarInstance *RegistrarInstance `json:"-"`
DNSProviderInstances []*DNSProviderInstance `json:"-"`
}
// Copy returns a deep copy of the DomainConfig.
func (dc *DomainConfig) Copy() (*DomainConfig, error) {
newDc := &DomainConfig{}
// provider instances are interfaces that gob hates if you don't register them.
// and the specific types are not gob encodable since nothing is exported.
// should find a better solution for this now.
//
// current strategy: remove everything, gob copy it. Then set both to stored copy.
reg := dc.RegistrarInstance
dnsps := dc.DNSProviderInstances
dc.RegistrarInstance = nil
dc.DNSProviderInstances = nil
err := copyObj(dc, newDc)
dc.RegistrarInstance = reg
newDc.RegistrarInstance = reg
dc.DNSProviderInstances = dnsps
newDc.DNSProviderInstances = dnsps
return newDc, err
}
// HasRecordTypeName returns True if there is a record with this rtype and name.
func (dc *DomainConfig) HasRecordTypeName(rtype, name string) bool {
for _, r := range dc.Records {
if r.Type == rtype && r.Name == name {
return true
}
}
return false
}
// Filter removes all records that don't match the filter f.
func (dc *DomainConfig) Filter(f func(r *RecordConfig) bool) {
recs := []*RecordConfig{}
for _, r := range dc.Records {
if f(r) {
recs = append(recs, r)
}
}
dc.Records = recs
}
// Punycode will convert all records to punycode format.
// It will encode:
// - Name
// - NameFQDN
// - Target (CNAME and MX only)
func (dc *DomainConfig) Punycode() error {
var err error
for _, rec := range dc.Records {
rec.Name, err = idna.ToASCII(rec.Name)
if err != nil {
return err
}
rec.NameFQDN, err = idna.ToASCII(rec.NameFQDN)
if err != nil {
return err
}
switch rec.Type { // #rtype_variations
case "ALIAS", "MX", "NS", "CNAME", "PTR", "SRV", "URL", "URL301", "FRAME", "R53_ALIAS":
rec.Target, err = idna.ToASCII(rec.Target)
if err != nil {
return err
}
case "A", "AAAA", "CAA", "TXT", "TLSA":
// Nothing to do.
default:
msg := fmt.Sprintf("Punycode rtype %v unimplemented", rec.Type)
panic(msg)
// We panic so that we quickly find any switch statements
// that have not been updated for a new RR type.
}
}
return nil
}

View File

@@ -1,25 +1,30 @@
package models
// DNSProvider is an interface for DNS Provider plug-ins.
type DNSProvider interface {
GetNameservers(domain string) ([]*Nameserver, error)
GetDomainCorrections(dc *DomainConfig) ([]*Correction, error)
}
// Registrar is an interface for Registrar plug-ins.
type Registrar interface {
GetRegistrarCorrections(dc *DomainConfig) ([]*Correction, error)
}
// ProviderBase describes providers.
type ProviderBase struct {
Name string
IsDefault bool
ProviderType string
}
// RegistrarInstance is a single registrar.
type RegistrarInstance struct {
ProviderBase
Driver Registrar
}
// DNSProviderInstance is a single DNS provider.
type DNSProviderInstance struct {
ProviderBase
Driver DNSProvider

View File

@@ -2,26 +2,6 @@ package models
import "strings"
// SetTxt sets the value of a TXT record to s.
func (rc *RecordConfig) SetTxt(s string) {
rc.Target = s
rc.TxtStrings = []string{s}
}
// SetTxts sets the value of a TXT record to the list of strings s.
func (rc *RecordConfig) SetTxts(s []string) {
rc.Target = s[0]
rc.TxtStrings = s
}
// SetTxtParse sets the value of TXT record if the list of strings is combined into one string.
// `foo` -> []string{"foo"}
// `"foo"` -> []string{"foo"}
// `"foo" "bar"` -> []string{"foo" "bar"}
func (rc *RecordConfig) SetTxtParse(s string) {
rc.SetTxts(ParseQuotedTxt(s))
}
// IsQuoted returns true if the string starts and ends with a double quote.
func IsQuoted(s string) bool {
if s == "" {
@@ -47,7 +27,7 @@ func StripQuotes(s string) string {
// ParseQuotedTxt returns the individual strings of a combined quoted string.
// `foo` -> []string{"foo"}
// `"foo"` -> []string{"foo"}
// `"foo" "bar"` -> []string{"foo" "bar"}
// `"foo" "bar"` -> []string{"foo", "bar"}
// NOTE: it is assumed there is exactly one space between the quotes.
func ParseQuotedTxt(s string) []string {
if !IsQuoted(s) {

View File

@@ -56,16 +56,24 @@ func TestSetTxtParse(t *testing.T) {
e1 string
e2 []string
}{
{``, ``, []string{``}},
{`foo`, `foo`, []string{`foo`}},
{`"foo"`, `foo`, []string{`foo`}},
{`"foo bar"`, `foo bar`, []string{`foo bar`}},
{`foo bar`, `foo bar`, []string{`foo bar`}},
{`"aaa" "bbb"`, `aaa`, []string{`aaa`, `bbb`}},
}
for i, test := range tests {
x := &RecordConfig{Type: "TXT"}
x.SetTxtParse(test.d1)
if x.Target != test.e1 {
t.Errorf("%v: expected Target=(%v) got (%v)", i, test.e1, x.Target)
ls := ParseQuotedTxt(test.d1)
if ls[0] != test.e1 {
t.Errorf("%v: expected Target=(%v) got (%v)", i, test.e1, ls[0])
}
if len(ls) != len(test.e2) {
t.Errorf("%v: expected TxtStrings=(%v) got (%v)", i, test.e2, ls)
}
for i := range ls {
if len(ls[i]) != len(test.e2[i]) {
t.Errorf("%v: expected TxtStrings=(%v) got (%v)", i, test.e2, ls)
}
}
}
}

277
models/record.go Normal file
View 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
}

46
models/t_caa.go Normal file
View File

@@ -0,0 +1,46 @@
package models
import (
"strconv"
"strings"
"github.com/pkg/errors"
)
// SetTargetCAA sets the CAA fields.
func (rc *RecordConfig) SetTargetCAA(flag uint8, tag string, target string) error {
rc.CaaTag = tag
rc.CaaFlag = flag
rc.Target = target
if rc.Type == "" {
rc.Type = "CAA"
}
if rc.Type != "CAA" {
panic("assertion failed: SetTargetCAA called when .Type is not CAA")
}
if tag != "issue" && tag != "issuewild" && tag != "iodef" {
return errors.Errorf("CAA tag (%v) is not one of issue/issuewild/iodef", tag)
}
return nil
}
// SetTargetCAAStrings is like SetTargetCAA but accepts strings.
func (rc *RecordConfig) SetTargetCAAStrings(flag, tag, target string) error {
i64flag, err := strconv.ParseUint(flag, 10, 8)
if err != nil {
return errors.Wrap(err, "CAA flag does not fit in 8 bits")
}
return rc.SetTargetCAA(uint8(i64flag), tag, target)
}
// SetTargetCAAString is like SetTargetCAA but accepts one big string.
// Ex: `0 issue "letsencrypt.org"`
func (rc *RecordConfig) SetTargetCAAString(s string) error {
part := strings.Fields(s)
if len(part) != 3 {
return errors.Errorf("CAA value does not contain 3 fields: (%#v)", s)
}
return rc.SetTargetCAAStrings(part[0], part[1], StripQuotes(part[2]))
}

40
models/t_mx.go Normal file
View File

@@ -0,0 +1,40 @@
package models
import (
"strconv"
"strings"
"github.com/pkg/errors"
)
// SetTargetMX sets the MX fields.
func (rc *RecordConfig) SetTargetMX(pref uint16, target string) error {
rc.MxPreference = pref
rc.Target = target
if rc.Type == "" {
rc.Type = "MX"
}
if rc.Type != "MX" {
panic("assertion failed: SetTargetMX called when .Type is not MX")
}
return nil
}
// SetTargetMXStrings is like SetTargetMX but accepts strings.
func (rc *RecordConfig) SetTargetMXStrings(pref, target string) error {
u64pref, err := strconv.ParseUint(pref, 10, 16)
if err != nil {
return errors.Wrap(err, "can't parse MX data")
}
return rc.SetTargetMX(uint16(u64pref), target)
}
// SetTargetMXString is like SetTargetMX but accepts one big string.
func (rc *RecordConfig) SetTargetMXString(s string) error {
part := strings.Fields(s)
if len(part) != 2 {
return errors.Errorf("MX value does not contain 2 fields: (%#v)", s)
}
return rc.SetTargetMXStrings(part[0], part[1])
}

53
models/t_parse.go Normal file
View File

@@ -0,0 +1,53 @@
package models
import (
"net"
"github.com/pkg/errors"
)
// PopulateFromString populates a RecordConfig given a type and string.
// Many providers give all the parameters of a resource record in one big
// string (all the parameters of an MX, SRV, CAA, etc). Rather than have
// each provider rewrite this code many times, here's a helper function to use.
//
// At this time, the idiom is to panic rather than continue with potentially
// misunderstood data. We do this panic() at the provider level.
// Therefore the typical calling sequence is:
// if err := rc.PopulateFromString(rtype, value, origin); err != nil {
// panic(errors.Wrap(err, "unparsable record received from provider"))
// }
func (r *RecordConfig) PopulateFromString(rtype, contents, origin string) error {
if r.Type != "" && r.Type != rtype {
panic(errors.Errorf("assertion failed: rtype already set (%s) (%s)", rtype, r.Type))
}
switch r.Type = rtype; rtype { // #rtype_variations
case "A":
ip := net.ParseIP(contents)
if ip == nil || ip.To4() == nil {
return errors.Errorf("A record with invalid IP: %s", contents)
}
return r.SetTargetIP(ip) // Reformat to canonical form.
case "AAAA":
ip := net.ParseIP(contents)
if ip == nil || ip.To16() == nil {
return errors.Errorf("AAAA record with invalid IP: %s", contents)
}
return r.SetTargetIP(ip) // Reformat to canonical form.
case "ANAME", "CNAME", "NS", "PTR":
return r.SetTarget(contents)
case "CAA":
return r.SetTargetCAAString(contents)
case "MX":
return r.SetTargetMXString(contents)
case "SRV":
return r.SetTargetSRVString(contents)
case "TLSA":
return r.SetTargetTLSAString(contents)
case "TXT":
return r.SetTargetTXTString(contents)
default:
return errors.Errorf("Unknown rtype (%s) when parsing (%s) domain=(%s)",
rtype, contents, origin)
}
}

64
models/t_srv.go Normal file
View File

@@ -0,0 +1,64 @@
package models
import (
"strconv"
"strings"
"github.com/pkg/errors"
)
// SetTargetSRV sets the SRV fields.
func (rc *RecordConfig) SetTargetSRV(priority, weight, port uint16, target string) error {
rc.SrvPriority = priority
rc.SrvWeight = weight
rc.SrvPort = port
rc.Target = target
if rc.Type == "" {
rc.Type = "SRV"
}
if rc.Type != "SRV" {
panic("assertion failed: SetTargetSRV called when .Type is not SRV")
}
return nil
}
// setTargetIntAndStrings is like SetTargetSRV but accepts priority as an int, the other parameters as strings.
func (rc *RecordConfig) setTargetIntAndStrings(priority uint16, weight, port, target string) (err error) {
var i64weight, i64port uint64
if i64weight, err = strconv.ParseUint(weight, 10, 16); err == nil {
if i64port, err = strconv.ParseUint(port, 10, 16); err == nil {
return rc.SetTargetSRV(priority, uint16(i64weight), uint16(i64port), target)
}
}
return errors.Wrap(err, "SRV value too big for uint16")
}
// SetTargetSRVStrings is like SetTargetSRV but accepts all parameters as strings.
func (rc *RecordConfig) SetTargetSRVStrings(priority, weight, port, target string) (err error) {
var i64priority uint64
if i64priority, err = strconv.ParseUint(priority, 10, 16); err == nil {
return rc.setTargetIntAndStrings(uint16(i64priority), weight, port, target)
}
return errors.Wrap(err, "SRV value too big for uint16")
}
// SetTargetSRVPriorityString is like SetTargetSRV but accepts priority as an
// uint16 and the rest of the values joined in a string that needs to be parsed.
// This is a helper function that comes in handy when a provider re-uses the MX preference
// field as the SRV priority.
func (rc *RecordConfig) SetTargetSRVPriorityString(priority uint16, s string) error {
part := strings.Fields(s)
if len(part) != 3 {
return errors.Errorf("SRV value does not contain 3 fields: (%#v)", s)
}
return rc.setTargetIntAndStrings(priority, part[0], part[1], part[2])
}
// SetTargetSRVString is like SetTargetSRV but accepts one big string to be parsed.
func (rc *RecordConfig) SetTargetSRVString(s string) error {
part := strings.Fields(s)
if len(part) != 4 {
return errors.Errorf("SRC value does not contain 4 fields: (%#v)", s)
}
return rc.SetTargetSRVStrings(part[0], part[1], part[2], part[3])
}

45
models/t_tlsa.go Normal file
View File

@@ -0,0 +1,45 @@
package models
import (
"strconv"
"strings"
"github.com/pkg/errors"
)
// SetTargetTLSA sets the TLSA fields.
func (rc *RecordConfig) SetTargetTLSA(usage, selector, matchingtype uint8, target string) error {
rc.TlsaUsage = usage
rc.TlsaSelector = selector
rc.TlsaMatchingType = matchingtype
rc.Target = target
if rc.Type == "" {
rc.Type = "TLSA"
}
if rc.Type != "TLSA" {
panic("assertion failed: SetTargetTLSA called when .Type is not TLSA")
}
return nil
}
// SetTargetTLSAStrings is like SetTargetTLSA but accepts strings.
func (rc *RecordConfig) SetTargetTLSAStrings(usage, selector, matchingtype, target string) (err error) {
var i64usage, i64selector, i64matchingtype uint64
if i64usage, err = strconv.ParseUint(usage, 10, 8); err == nil {
if i64selector, err = strconv.ParseUint(selector, 10, 8); err == nil {
if i64matchingtype, err = strconv.ParseUint(matchingtype, 10, 8); err == nil {
return rc.SetTargetTLSA(uint8(i64usage), uint8(i64selector), uint8(i64matchingtype), target)
}
}
}
return errors.Wrap(err, "TLSA has value that won't fit in field")
}
// SetTargetTLSAString is like SetTargetTLSA but accepts one big string.
func (rc *RecordConfig) SetTargetTLSAString(s string) error {
part := strings.Fields(s)
if len(part) != 4 {
return errors.Errorf("TLSA value does not contain 4 fields: (%#v)", s)
}
return rc.SetTargetTLSAStrings(part[0], part[1], part[2], part[3])
}

35
models/t_txt.go Normal file
View File

@@ -0,0 +1,35 @@
package models
// SetTargetTXT sets the TXT fields when there is 1 string.
func (rc *RecordConfig) SetTargetTXT(s string) error {
rc.Target = s
rc.TxtStrings = []string{s}
if rc.Type == "" {
rc.Type = "TXT"
}
if rc.Type != "TXT" {
panic("assertion failed: SetTargetTXT called when .Type is not TXT")
}
return nil
}
// SetTargetTXTs sets the TXT fields when there are many strings.
func (rc *RecordConfig) SetTargetTXTs(s []string) error {
rc.Target = s[0]
rc.TxtStrings = s
if rc.Type == "" {
rc.Type = "TXT"
}
if rc.Type != "TXT" {
panic("assertion failed: SetTargetTXT called when .Type is not TXT")
}
return nil
}
// SetTargetTXTString is like SetTargetTXT but accepts one big string.
// Ex: foo << 1 string
// foo bar << 1 string
// "foo" "bar" << 2 strings
func (rc *RecordConfig) SetTargetTXTString(s string) error {
return rc.SetTargetTXTs(ParseQuotedTxt(s))
}

115
models/target.go Normal file
View File

@@ -0,0 +1,115 @@
package models
import (
"net"
"strings"
"github.com/miekg/dns"
"github.com/pkg/errors"
)
/* .Target is kind of a mess.
For simple rtypes it is the record's value. (i.e. for an A record
it is the IP address).
For complex rtypes (like an MX record has a preference and a value)
it might be a space-delimited string with all the parameters, or it
might just be the hostname.
This was a bad design decision that I regret. Eventually we will eliminate this
field and replace it with setters/getters. The setters/getters are below
so that it is easy to do things the right way in preparation.
*/
// GetTargetField returns the target. There may be other fields (for example
// an MX record also has a .MxPreference field.
func (rc *RecordConfig) GetTargetField() string {
return rc.Target
}
// // GetTargetSingle returns the target for types that have a single value target
// // and panics for all others.
// func (rc *RecordConfig) GetTargetSingle() string {
// if rc.Type == "MX" || rc.Type == "SRV" || rc.Type == "CAA" || rc.Type == "TLSA" || rc.Type == "TXT" {
// panic("TargetSingle called on a type with a multi-parameter rtype.")
// }
// return rc.Target
// }
// GetTargetIP returns the net.IP stored in Target.
func (rc *RecordConfig) GetTargetIP() net.IP {
if rc.Type != "A" && rc.Type != "AAAA" {
panic(errors.Errorf("GetTargetIP called on an inappropriate rtype (%s)", rc.Type))
}
return net.ParseIP(rc.Target)
}
// GetTargetCombined returns a string with the various fields combined.
// For example, an MX record might output `10 mx10.example.tld`.
func (rc *RecordConfig) GetTargetCombined() string {
// If this is a pseudo record, just return the target.
if _, ok := dns.StringToType[rc.Type]; !ok {
return rc.Target
}
// 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.
// Sadly String() always includes a header, which we must strip out.
// TODO(tlim): Request the dns project add a function that returns
// the string without the header.
rr := rc.ToRR()
header := rr.Header().String()
full := rr.String()
if !strings.HasPrefix(full, header) {
panic("assertion failed. dns.Hdr.String() behavior has changed in an incompatible way")
}
return full[len(header):]
}
// // GetTargetDebug returns a string with the various fields spelled out.
// func (rc *RecordConfig) GetTargetDebug() string {
// content := fmt.Sprintf("%s %s %s %d", rc.Type, rc.NameFQDN, rc.Target, rc.TTL)
// switch rc.Type { // #rtype_variations
// case "A", "AAAA", "CNAME", "NS", "PTR", "TXT":
// // Nothing special.
// case "MX":
// content += fmt.Sprintf(" pref=%d", rc.MxPreference)
// case "SOA":
// content = fmt.Sprintf("%s %s %s %d", rc.Type, rc.Name, rc.Target, rc.TTL)
// case "SRV":
// content += fmt.Sprintf(" srvpriority=%d srvweight=%d srvport=%d", rc.SrvPriority, rc.SrvWeight, rc.SrvPort)
// case "TLSA":
// content += fmt.Sprintf(" tlsausage=%d tlsaselector=%d tlsamatchingtype=%d", rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType)
// case "CAA":
// content += fmt.Sprintf(" caatag=%s caaflag=%d", rc.CaaTag, rc.CaaFlag)
// case "R53_ALIAS":
// content += fmt.Sprintf(" type=%s zone_id=%s", rc.R53Alias["type"], rc.R53Alias["zone_id"])
// default:
// panic(errors.Errorf("rc.String rtype %v unimplemented", rc.Type))
// // We panic so that we quickly find any switch statements
// // that have not been updated for a new RR type.
// }
// for k, v := range rc.Metadata {
// content += fmt.Sprintf(" %s=%s", k, v)
// }
// return content
// }
// SetTarget sets the target (assumes that the rtype is appropriate).
func (rc *RecordConfig) SetTarget(fqdn string) error {
rc.Target = fqdn
return nil
}
// SetTargetIP sets the target to an IP, verifying this is an appropriate rtype.
func (rc *RecordConfig) SetTargetIP(ip net.IP) error {
// TODO(tlim): Verify the rtype is appropriate for an IP.
rc.Target = ip.String()
return nil
}
// // SetTargetFQDN sets the target to an IP, verifying this is an appropriate rtype.
// func (rc *RecordConfig) SetTargetFQDN(target string) error {
// // TODO(tlim): Verify the rtype is appropriate for an hostname.
// rc.Target = target
// return nil
// }

29
models/util.go Normal file
View File

@@ -0,0 +1,29 @@
package models
import (
"bytes"
"encoding/gob"
"strconv"
"github.com/pkg/errors"
)
func copyObj(input interface{}, output interface{}) error {
buf := &bytes.Buffer{}
enc := gob.NewEncoder(buf)
dec := gob.NewDecoder(buf)
if err := enc.Encode(input); err != nil {
return err
}
return dec.Decode(output)
}
// atou32 converts a string to uint32 or panics.
// DEPRECATED: This will go away when SOA record handling is rewritten.
func atou32(s string) uint32 {
i64, err := strconv.ParseUint(s, 10, 32)
if err != nil {
panic(errors.Errorf("atou32 failed (%v) (err=%v", s, err))
}
return uint32(i64)
}