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:
540
models/dns.go
540
models/dns.go
@@ -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
102
models/domain.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
@@ -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
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
|
||||
}
|
||||
46
models/t_caa.go
Normal file
46
models/t_caa.go
Normal 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
40
models/t_mx.go
Normal 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
53
models/t_parse.go
Normal 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
64
models/t_srv.go
Normal 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
45
models/t_tlsa.go
Normal 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
35
models/t_txt.go
Normal 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
115
models/target.go
Normal 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
29
models/util.go
Normal 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)
|
||||
}
|
||||
@@ -35,7 +35,11 @@ func flattenSPFs(cfg *models.DNSConfig) []error {
|
||||
}
|
||||
if flatten, ok := txt.Metadata["flatten"]; ok && strings.HasPrefix(txt.Target, "v=spf1") {
|
||||
rec = rec.Flatten(flatten)
|
||||
txt.SetTxt(rec.TXT())
|
||||
err = txt.SetTargetTXT(rec.TXT())
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// now split if needed
|
||||
if split, ok := txt.Metadata["split"]; ok {
|
||||
@@ -46,10 +50,10 @@ func flattenSPFs(cfg *models.DNSConfig) []error {
|
||||
recs := rec.TXTSplit(split + "." + domain.Name)
|
||||
for k, v := range recs {
|
||||
if k == "@" {
|
||||
txt.SetTxt(v)
|
||||
txt.SetTargetTXT(v)
|
||||
} else {
|
||||
cp, _ := txt.Copy()
|
||||
cp.SetTxt(v)
|
||||
cp.SetTargetTXT(v)
|
||||
cp.NameFQDN = k
|
||||
cp.Name = dnsutil.TrimDomainName(k, domain.Name)
|
||||
domain.Records = append(domain.Records, cp)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package normalize
|
||||
|
||||
import (
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"testing"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
)
|
||||
|
||||
func TestImportTransform(t *testing.T) {
|
||||
@@ -13,14 +14,14 @@ func TestImportTransform(t *testing.T) {
|
||||
Name: "stackexchange.com",
|
||||
Records: []*models.RecordConfig{
|
||||
{Type: "A", Name: "*", NameFQDN: "*.stackexchange.com", Target: "0.0.2.2"},
|
||||
{Type: "A", Name: "www", NameFQDN: "", Target: "0.0.1.1"},
|
||||
{Type: "A", Name: "www", NameFQDN: "www.stackexchange.com", Target: "0.0.1.1"},
|
||||
},
|
||||
}
|
||||
dst := &models.DomainConfig{
|
||||
Name: "internal",
|
||||
Records: []*models.RecordConfig{
|
||||
{Type: "A", Name: "*.stackexchange.com", NameFQDN: "*.stackexchange.com.internal", Target: "0.0.3.3", Metadata: map[string]string{"transform_table": transformSingle}},
|
||||
{Type: "IMPORT_TRANSFORM", Name: "@", Target: "stackexchange.com", Metadata: map[string]string{"transform_table": transformDouble}},
|
||||
{Type: "IMPORT_TRANSFORM", Name: "@", NameFQDN: "internal", Target: "stackexchange.com", Metadata: map[string]string{"transform_table": transformDouble}},
|
||||
},
|
||||
}
|
||||
cfg := &models.DNSConfig{
|
||||
|
||||
@@ -196,7 +196,7 @@ func TestCAAValidation(t *testing.T) {
|
||||
Name: "example.com",
|
||||
RegistrarName: "BIND",
|
||||
Records: []*models.RecordConfig{
|
||||
{Name: "@", Type: "CAA", CaaTag: "invalid", Target: "example.com"},
|
||||
{Name: "@", NameFQDN: "example.com", Type: "CAA", CaaTag: "invalid", Target: "example.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -214,7 +214,7 @@ func TestTLSAValidation(t *testing.T) {
|
||||
Name: "_443._tcp.example.com",
|
||||
RegistrarName: "BIND",
|
||||
Records: []*models.RecordConfig{
|
||||
{Name: "_443._tcp", Type: "TLSA", TlsaUsage: 4, TlsaSelector: 1, TlsaMatchingType: 1, Target: "abcdef0"},
|
||||
{Name: "_443._tcp", NameFQDN: "_443._tcp._443._tcp.example.com", Type: "TLSA", TlsaUsage: 4, TlsaSelector: 1, TlsaMatchingType: 1, Target: "abcdef0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,12 +8,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"github.com/StackExchange/dnscontrol/providers/diff"
|
||||
"github.com/TomOnTime/utfutil"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"github.com/StackExchange/dnscontrol/providers/diff"
|
||||
)
|
||||
|
||||
const zoneDumpFilenamePrefix = "adzonedump"
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"github.com/StackExchange/dnscontrol/providers"
|
||||
@@ -99,29 +99,26 @@ func rrToRecord(rr dns.RR, origin string, replaceSerial uint32) (models.RecordCo
|
||||
// If one is found, we replace it with serial=1.
|
||||
var oldSerial, newSerial uint32
|
||||
header := rr.Header()
|
||||
rc := models.RecordConfig{}
|
||||
rc.Type = dns.TypeToString[header.Rrtype]
|
||||
rc.NameFQDN = strings.ToLower(strings.TrimSuffix(header.Name, "."))
|
||||
rc.Name = strings.ToLower(dnsutil.TrimDomainName(header.Name, origin))
|
||||
rc.TTL = header.Ttl
|
||||
rc := models.RecordConfig{
|
||||
Type: dns.TypeToString[header.Rrtype],
|
||||
TTL: header.Ttl,
|
||||
}
|
||||
rc.SetLabelFromFQDN(strings.TrimSuffix(header.Name, "."), origin)
|
||||
switch v := rr.(type) { // #rtype_variations
|
||||
case *dns.A:
|
||||
rc.Target = v.A.String()
|
||||
panicInvalid(rc.SetTarget(v.A.String()))
|
||||
case *dns.AAAA:
|
||||
rc.Target = v.AAAA.String()
|
||||
panicInvalid(rc.SetTarget(v.AAAA.String()))
|
||||
case *dns.CAA:
|
||||
rc.CaaTag = v.Tag
|
||||
rc.CaaFlag = v.Flag
|
||||
rc.Target = v.Value
|
||||
panicInvalid(rc.SetTargetCAA(v.Flag, v.Tag, v.Value))
|
||||
case *dns.CNAME:
|
||||
rc.Target = v.Target
|
||||
panicInvalid(rc.SetTarget(v.Target))
|
||||
case *dns.MX:
|
||||
rc.Target = v.Mx
|
||||
rc.MxPreference = v.Preference
|
||||
panicInvalid(rc.SetTargetMX(v.Preference, v.Mx))
|
||||
case *dns.NS:
|
||||
rc.Target = v.Ns
|
||||
panicInvalid(rc.SetTarget(v.Ns))
|
||||
case *dns.PTR:
|
||||
rc.Target = v.Ptr
|
||||
panicInvalid(rc.SetTarget(v.Ptr))
|
||||
case *dns.SOA:
|
||||
oldSerial = v.Serial
|
||||
if oldSerial == 0 {
|
||||
@@ -129,37 +126,39 @@ func rrToRecord(rr dns.RR, origin string, replaceSerial uint32) (models.RecordCo
|
||||
oldSerial = 1
|
||||
}
|
||||
newSerial = v.Serial
|
||||
if (dnsutil.TrimDomainName(rc.Name, origin+".") == "@") && replaceSerial != 0 {
|
||||
//if (dnsutil.TrimDomainName(rc.Name, origin+".") == "@") && replaceSerial != 0 {
|
||||
if rc.Name == "@" && replaceSerial != 0 {
|
||||
newSerial = replaceSerial
|
||||
}
|
||||
rc.Target = fmt.Sprintf("%v %v %v %v %v %v %v",
|
||||
v.Ns, v.Mbox, newSerial, v.Refresh, v.Retry, v.Expire, v.Minttl)
|
||||
panicInvalid(rc.SetTarget(
|
||||
fmt.Sprintf("%v %v %v %v %v %v %v",
|
||||
v.Ns, v.Mbox, newSerial, v.Refresh, v.Retry, v.Expire, v.Minttl),
|
||||
))
|
||||
// FIXME(tlim): SOA should be handled by splitting out the fields.
|
||||
case *dns.SRV:
|
||||
rc.Target = v.Target
|
||||
rc.SrvPort = v.Port
|
||||
rc.SrvWeight = v.Weight
|
||||
rc.SrvPriority = v.Priority
|
||||
panicInvalid(rc.SetTargetSRV(v.Priority, v.Weight, v.Port, v.Target))
|
||||
case *dns.TLSA:
|
||||
rc.TlsaUsage = v.Usage
|
||||
rc.TlsaSelector = v.Selector
|
||||
rc.TlsaMatchingType = v.MatchingType
|
||||
rc.Target = v.Certificate
|
||||
panicInvalid(rc.SetTargetTLSA(v.Usage, v.Selector, v.MatchingType, v.Certificate))
|
||||
case *dns.TXT:
|
||||
rc.Target = strings.Join(v.Txt, " ")
|
||||
rc.TxtStrings = v.Txt
|
||||
panicInvalid(rc.SetTargetTXTs(v.Txt))
|
||||
default:
|
||||
log.Fatalf("rrToRecord: Unimplemented zone record type=%s (%v)\n", rc.Type, rr)
|
||||
}
|
||||
return rc, oldSerial
|
||||
}
|
||||
|
||||
func panicInvalid(err error) {
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "unparsable record received from BIND"))
|
||||
}
|
||||
}
|
||||
|
||||
func makeDefaultSOA(info SoaInfo, origin string) *models.RecordConfig {
|
||||
// Make a default SOA record in case one isn't found:
|
||||
soaRec := models.RecordConfig{
|
||||
Type: "SOA",
|
||||
Name: "@",
|
||||
}
|
||||
soaRec.NameFQDN = dnsutil.AddOrigin(soaRec.Name, origin)
|
||||
soaRec.SetLabel("@", origin)
|
||||
if len(info.Ns) == 0 {
|
||||
info.Ns = "DEFAULT_NOT_SET."
|
||||
}
|
||||
@@ -181,7 +180,7 @@ func makeDefaultSOA(info SoaInfo, origin string) *models.RecordConfig {
|
||||
if info.Minttl == 0 {
|
||||
info.Minttl = 1440
|
||||
}
|
||||
soaRec.Target = info.String()
|
||||
soaRec.SetTarget(info.String())
|
||||
|
||||
return &soaRec
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
type zoneGenData struct {
|
||||
Origin string
|
||||
DefaultTtl uint32
|
||||
DefaultTTL uint32
|
||||
Records []dns.RR
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ func WriteZoneFile(w io.Writer, records []dns.RR, origin string) error {
|
||||
|
||||
z := &zoneGenData{
|
||||
Origin: dnsutil.AddOrigin(origin, "."),
|
||||
DefaultTtl: defaultTTL,
|
||||
DefaultTTL: defaultTTL,
|
||||
}
|
||||
z.Records = nil
|
||||
for _, r := range records {
|
||||
@@ -161,7 +161,7 @@ func (z *zoneGenData) generateZoneFileHelper(w io.Writer) error {
|
||||
nameShortPrevious := ""
|
||||
|
||||
sort.Sort(z)
|
||||
fmt.Fprintln(w, "$TTL", z.DefaultTtl)
|
||||
fmt.Fprintln(w, "$TTL", z.DefaultTTL)
|
||||
for i, rr := range z.Records {
|
||||
line := rr.String()
|
||||
if line[0] == ';' {
|
||||
@@ -187,7 +187,7 @@ func (z *zoneGenData) generateZoneFileHelper(w io.Writer) error {
|
||||
|
||||
// items[1]: ttl
|
||||
ttl := ""
|
||||
if hdr.Ttl != z.DefaultTtl && hdr.Ttl != 0 {
|
||||
if hdr.Ttl != z.DefaultTTL && hdr.Ttl != 0 {
|
||||
ttl = items[1]
|
||||
}
|
||||
|
||||
|
||||
@@ -121,8 +121,8 @@ func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models
|
||||
if rec.Type == "ALIAS" {
|
||||
rec.Type = "CNAME"
|
||||
}
|
||||
if labelMatches(rec.Name, c.ignoredLabels) {
|
||||
log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.Name, c.ignoredLabels)
|
||||
if labelMatches(rec.GetLabel(), c.ignoredLabels) {
|
||||
log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.GetLabel(), c.ignoredLabels)
|
||||
}
|
||||
}
|
||||
checkNSModifications(dc)
|
||||
@@ -151,7 +151,7 @@ func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models
|
||||
if des.Type == "PAGE_RULE" {
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: d.String(),
|
||||
F: func() error { return c.createPageRule(id, des.Target) },
|
||||
F: func() error { return c.createPageRule(id, des.GetTargetField()) },
|
||||
})
|
||||
} else {
|
||||
corrections = append(corrections, c.createRec(des, id)...)
|
||||
@@ -164,7 +164,7 @@ func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models
|
||||
if rec.Type == "PAGE_RULE" {
|
||||
corrections = append(corrections, &models.Correction{
|
||||
Msg: d.String(),
|
||||
F: func() error { return c.updatePageRule(ex.Original.(*pageRule).ID, id, rec.Target) },
|
||||
F: func() error { return c.updatePageRule(ex.Original.(*pageRule).ID, id, rec.GetTargetField()) },
|
||||
})
|
||||
} else {
|
||||
e := ex.Original.(*cfRecord)
|
||||
@@ -181,9 +181,9 @@ func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models
|
||||
func checkNSModifications(dc *models.DomainConfig) {
|
||||
newList := make([]*models.RecordConfig, 0, len(dc.Records))
|
||||
for _, rec := range dc.Records {
|
||||
if rec.Type == "NS" && rec.NameFQDN == dc.Name {
|
||||
if !strings.HasSuffix(rec.Target, ".ns.cloudflare.com.") {
|
||||
log.Printf("Warning: cloudflare does not support modifying NS records on base domain. %s will not be added.", rec.Target)
|
||||
if rec.Type == "NS" && rec.GetLabelFQDN() == dc.Name {
|
||||
if !strings.HasSuffix(rec.GetTargetField(), ".ns.cloudflare.com.") {
|
||||
log.Printf("Warning: cloudflare does not support modifying NS records on base domain. %s will not be added.", rec.GetTargetField())
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -240,7 +240,7 @@ func (c *CloudflareApi) preprocessConfig(dc *models.DomainConfig) error {
|
||||
}
|
||||
if rec.Type != "A" && rec.Type != "CNAME" && rec.Type != "AAAA" && rec.Type != "ALIAS" {
|
||||
if rec.Metadata[metaProxy] != "" {
|
||||
return errors.Errorf("cloudflare_proxy set on %v record: %#v cloudflare_proxy=%#v", rec.Type, rec.Name, rec.Metadata[metaProxy])
|
||||
return errors.Errorf("cloudflare_proxy set on %v record: %#v cloudflare_proxy=%#v", rec.Type, rec.GetLabel(), rec.Metadata[metaProxy])
|
||||
}
|
||||
// Force it to off.
|
||||
rec.Metadata[metaProxy] = "off"
|
||||
@@ -260,7 +260,7 @@ func (c *CloudflareApi) preprocessConfig(dc *models.DomainConfig) error {
|
||||
if !c.manageRedirects {
|
||||
return errors.Errorf("you must add 'manage_redirects: true' metadata to cloudflare provider to use CF_REDIRECT records")
|
||||
}
|
||||
parts := strings.Split(rec.Target, ",")
|
||||
parts := strings.Split(rec.GetTargetField(), ",")
|
||||
if len(parts) != 2 {
|
||||
return errors.Errorf("Invalid data specified for cloudflare redirect record")
|
||||
}
|
||||
@@ -268,7 +268,7 @@ func (c *CloudflareApi) preprocessConfig(dc *models.DomainConfig) error {
|
||||
if rec.Type == "CF_TEMP_REDIRECT" {
|
||||
code = 302
|
||||
}
|
||||
rec.Target = fmt.Sprintf("%s,%d,%d", rec.Target, currentPrPrio, code)
|
||||
rec.SetTarget(fmt.Sprintf("%s,%d,%d", rec.GetTargetField(), currentPrPrio, code))
|
||||
currentPrPrio++
|
||||
rec.Type = "PAGE_RULE"
|
||||
}
|
||||
@@ -283,16 +283,16 @@ func (c *CloudflareApi) preprocessConfig(dc *models.DomainConfig) error {
|
||||
if rec.Metadata[metaProxy] != "full" {
|
||||
continue
|
||||
}
|
||||
ip := net.ParseIP(rec.Target)
|
||||
ip := net.ParseIP(rec.GetTargetField())
|
||||
if ip == nil {
|
||||
return errors.Errorf("%s is not a valid ip address", rec.Target)
|
||||
return errors.Errorf("%s is not a valid ip address", rec.GetTargetField())
|
||||
}
|
||||
newIP, err := transform.TransformIP(ip, c.ipConversions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rec.Metadata[metaOriginalIP] = rec.Target
|
||||
rec.Target = newIP.String()
|
||||
rec.Metadata[metaOriginalIP] = rec.GetTargetField()
|
||||
rec.SetTarget(newIP.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -371,39 +371,32 @@ type cfRecord struct {
|
||||
Priority uint16 `json:"priority"`
|
||||
}
|
||||
|
||||
func (c *cfRecord) toRecord(domain string) *models.RecordConfig {
|
||||
func (c *cfRecord) nativeToRecord(domain string) *models.RecordConfig {
|
||||
// normalize cname,mx,ns records with dots to be consistent with our config format.
|
||||
if c.Type == "CNAME" || c.Type == "MX" || c.Type == "NS" || c.Type == "SRV" {
|
||||
c.Content = dnsutil.AddOrigin(c.Content+".", domain)
|
||||
}
|
||||
|
||||
rc := &models.RecordConfig{
|
||||
NameFQDN: c.Name,
|
||||
Type: c.Type,
|
||||
Target: c.Content,
|
||||
TTL: c.TTL,
|
||||
Original: c,
|
||||
}
|
||||
switch c.Type { // #rtype_variations
|
||||
case "A", "AAAA", "ANAME", "CNAME", "NS", "TXT":
|
||||
// nothing additional needed.
|
||||
case "CAA":
|
||||
var err error
|
||||
rc.CaaTag, rc.CaaFlag, rc.Target, err = models.SplitCombinedCaaValue(c.Content)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rc.SetLabelFromFQDN(c.Name, domain)
|
||||
switch rType := c.Type; rType { // #rtype_variations
|
||||
case "MX":
|
||||
rc.MxPreference = c.Priority
|
||||
if err := rc.SetTargetMX(c.Priority, c.Content); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable MX record received from cloudflare"))
|
||||
}
|
||||
case "SRV":
|
||||
data := *c.Data
|
||||
rc.SrvPriority = data.Priority
|
||||
rc.SrvWeight = data.Weight
|
||||
rc.SrvPort = data.Port
|
||||
rc.Target = dnsutil.AddOrigin(data.Target+".", domain)
|
||||
default:
|
||||
panic(fmt.Sprintf("toRecord unimplemented rtype %v", c.Type))
|
||||
// We panic so that we quickly find any switch statements
|
||||
// that have not been updated for a new RR type.
|
||||
if err := rc.SetTargetSRV(data.Priority, data.Weight, data.Port,
|
||||
dnsutil.AddOrigin(data.Target+".", domain)); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable SRV record received from cloudflare"))
|
||||
}
|
||||
default: // "A", "AAAA", "ANAME", "CAA", "CNAME", "NS", "PTR", "TXT"
|
||||
if err := rc.PopulateFromString(rType, c.Content, domain); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable record received from cloudflare"))
|
||||
}
|
||||
}
|
||||
|
||||
return rc
|
||||
|
||||
@@ -5,11 +5,9 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"strings"
|
||||
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"github.com/pkg/errors"
|
||||
@@ -69,7 +67,7 @@ func (c *CloudflareApi) getRecordsForDomain(id string, domain string) ([]*models
|
||||
}
|
||||
for _, rec := range data.Result {
|
||||
// fmt.Printf("REC: %+v\n", rec)
|
||||
records = append(records, rec.toRecord(domain))
|
||||
records = append(records, rec.nativeToRecord(domain))
|
||||
}
|
||||
ri := data.ResultInfo
|
||||
if len(data.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount {
|
||||
@@ -120,7 +118,7 @@ func (c *CloudflareApi) createZone(domainName string) (string, error) {
|
||||
}
|
||||
|
||||
func cfSrvData(rec *models.RecordConfig) *cfRecData {
|
||||
serverParts := strings.Split(rec.NameFQDN, ".")
|
||||
serverParts := strings.Split(rec.GetLabelFQDN(), ".")
|
||||
return &cfRecData{
|
||||
Service: serverParts[0],
|
||||
Proto: serverParts[1],
|
||||
@@ -128,7 +126,7 @@ func cfSrvData(rec *models.RecordConfig) *cfRecData {
|
||||
Port: rec.SrvPort,
|
||||
Priority: rec.SrvPriority,
|
||||
Weight: rec.SrvWeight,
|
||||
Target: rec.Target,
|
||||
Target: rec.GetTargetField(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +134,7 @@ func cfCaaData(rec *models.RecordConfig) *cfRecData {
|
||||
return &cfRecData{
|
||||
Tag: rec.CaaTag,
|
||||
Flags: rec.CaaFlag,
|
||||
Value: rec.Target,
|
||||
Value: rec.GetTargetField(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +148,7 @@ func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*
|
||||
Data *cfRecData `json:"data"`
|
||||
}
|
||||
var id string
|
||||
content := rec.Target
|
||||
content := rec.GetTargetField()
|
||||
if rec.Metadata[metaOriginalIP] != "" {
|
||||
content = rec.Metadata[metaOriginalIP]
|
||||
}
|
||||
@@ -159,11 +157,11 @@ func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*
|
||||
prio = fmt.Sprintf(" %d ", rec.MxPreference)
|
||||
}
|
||||
arr := []*models.Correction{{
|
||||
Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.Name, rec.Type, rec.TTL, prio, content),
|
||||
Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.GetLabel(), rec.Type, rec.TTL, prio, content),
|
||||
F: func() error {
|
||||
|
||||
cf := &createRecord{
|
||||
Name: rec.Name,
|
||||
Name: rec.GetLabel(),
|
||||
Type: rec.Type,
|
||||
TTL: rec.TTL,
|
||||
Content: content,
|
||||
@@ -171,10 +169,10 @@ func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*
|
||||
}
|
||||
if rec.Type == "SRV" {
|
||||
cf.Data = cfSrvData(rec)
|
||||
cf.Name = rec.NameFQDN
|
||||
cf.Name = rec.GetLabelFQDN()
|
||||
} else if rec.Type == "CAA" {
|
||||
cf.Data = cfCaaData(rec)
|
||||
cf.Name = rec.NameFQDN
|
||||
cf.Name = rec.GetLabelFQDN()
|
||||
cf.Content = ""
|
||||
}
|
||||
endpoint := fmt.Sprintf(recordsURL, domainID)
|
||||
@@ -194,7 +192,7 @@ func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*
|
||||
}}
|
||||
if rec.Metadata[metaProxy] != "off" {
|
||||
arr = append(arr, &models.Correction{
|
||||
Msg: fmt.Sprintf("ACTIVATE PROXY for new record %s %s %d %s", rec.Name, rec.Type, rec.TTL, rec.Target),
|
||||
Msg: fmt.Sprintf("ACTIVATE PROXY for new record %s %s %d %s", rec.GetLabel(), rec.Type, rec.TTL, rec.GetTargetField()),
|
||||
F: func() error { return c.modifyRecord(domainID, id, true, rec) },
|
||||
})
|
||||
}
|
||||
@@ -215,13 +213,22 @@ func (c *CloudflareApi) modifyRecord(domainID, recID string, proxied bool, rec *
|
||||
TTL uint32 `json:"ttl"`
|
||||
Data *cfRecData `json:"data"`
|
||||
}
|
||||
r := record{recID, proxied, rec.Name, rec.Type, rec.Target, rec.MxPreference, rec.TTL, nil}
|
||||
r := record{
|
||||
ID: recID,
|
||||
Proxied: proxied,
|
||||
Name: rec.GetLabel(),
|
||||
Type: rec.Type,
|
||||
Content: rec.GetTargetField(),
|
||||
Priority: rec.MxPreference,
|
||||
TTL: rec.TTL,
|
||||
Data: nil,
|
||||
}
|
||||
if rec.Type == "SRV" {
|
||||
r.Data = cfSrvData(rec)
|
||||
r.Name = rec.NameFQDN
|
||||
r.Name = rec.GetLabelFQDN()
|
||||
} else if rec.Type == "CAA" {
|
||||
r.Data = cfCaaData(rec)
|
||||
r.Name = rec.NameFQDN
|
||||
r.Name = rec.GetLabelFQDN()
|
||||
r.Content = ""
|
||||
}
|
||||
endpoint := fmt.Sprintf(singleRecordURL, domainID, recID)
|
||||
@@ -303,15 +310,18 @@ func (c *CloudflareApi) getPageRules(id string, domain string) ([]*models.Record
|
||||
return nil, err
|
||||
}
|
||||
var thisPr = pr
|
||||
recs = append(recs, &models.RecordConfig{
|
||||
Name: "@",
|
||||
NameFQDN: domain,
|
||||
r := &models.RecordConfig{
|
||||
Type: "PAGE_RULE",
|
||||
// $FROM,$TO,$PRIO,$CODE
|
||||
Target: fmt.Sprintf("%s,%s,%d,%d", pr.Targets[0].Constraint.Value, pr.ForwardingInfo.URL, pr.Priority, pr.ForwardingInfo.StatusCode),
|
||||
Original: thisPr,
|
||||
TTL: 1,
|
||||
})
|
||||
}
|
||||
r.SetLabel("@", domain)
|
||||
r.SetTarget(fmt.Sprintf("%s,%s,%d,%d", // $FROM,$TO,$PRIO,$CODE
|
||||
pr.Targets[0].Constraint.Value,
|
||||
pr.ForwardingInfo.URL,
|
||||
pr.Priority,
|
||||
pr.ForwardingInfo.StatusCode))
|
||||
recs = append(recs, r)
|
||||
}
|
||||
return recs, nil
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ type differ struct {
|
||||
|
||||
// get normalized content for record. target, ttl, mxprio, and specified metadata
|
||||
func (d *differ) content(r *models.RecordConfig) string {
|
||||
content := fmt.Sprintf("%v ttl=%d", r.Content(), r.TTL)
|
||||
content := fmt.Sprintf("%v ttl=%d", r.GetTargetCombined(), r.TTL)
|
||||
for _, f := range d.extraValues {
|
||||
// sort the extra values map keys to perform a deterministic
|
||||
// comparison since Golang maps iteration order is not guaranteed
|
||||
|
||||
@@ -197,10 +197,11 @@ func toRc(dc *models.DomainConfig, r *godo.DomainRecord) *models.RecordConfig {
|
||||
target = dc.Name
|
||||
}
|
||||
target = dnsutil.AddOrigin(target+".", dc.Name)
|
||||
// FIXME(tlim): The AddOrigin should be a no-op.
|
||||
// Test whether or not it is actually needed.
|
||||
}
|
||||
|
||||
return &models.RecordConfig{
|
||||
NameFQDN: name,
|
||||
t := &models.RecordConfig{
|
||||
Type: r.Type,
|
||||
Target: target,
|
||||
TTL: uint32(r.TTL),
|
||||
@@ -210,25 +211,37 @@ func toRc(dc *models.DomainConfig, r *godo.DomainRecord) *models.RecordConfig {
|
||||
SrvPort: uint16(r.Port),
|
||||
Original: r,
|
||||
}
|
||||
t.SetLabelFromFQDN(name, dc.Name)
|
||||
switch rtype := r.Type; rtype {
|
||||
case "TXT":
|
||||
t.SetTargetTXTString(target)
|
||||
default:
|
||||
// nothing additional required
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func toReq(dc *models.DomainConfig, rc *models.RecordConfig) *godo.DomainRecordEditRequest {
|
||||
// DO wants the short name, e.g. @
|
||||
name := dnsutil.TrimDomainName(rc.NameFQDN, dc.Name)
|
||||
name := rc.GetLabel() // DO wants the short name or "@" for apex.
|
||||
target := rc.GetTargetField() // DO uses the target field only for a single value
|
||||
priority := 0 // DO uses the same property for MX and SRV priority
|
||||
|
||||
// DO uses the same property for MX and SRV priority
|
||||
priority := 0
|
||||
switch rc.Type { // #rtype_variations
|
||||
case "MX":
|
||||
priority = int(rc.MxPreference)
|
||||
case "SRV":
|
||||
priority = int(rc.SrvPriority)
|
||||
case "TXT":
|
||||
// TXT records are the one place where DO combines many items into one field.
|
||||
target = rc.GetTargetCombined()
|
||||
default:
|
||||
// no action required
|
||||
}
|
||||
|
||||
return &godo.DomainRecordEditRequest{
|
||||
Type: rc.Type,
|
||||
Name: name,
|
||||
Data: rc.Target,
|
||||
Data: target,
|
||||
TTL: int(rc.TTL),
|
||||
Priority: priority,
|
||||
Port: int(rc.SrvPort),
|
||||
|
||||
@@ -79,25 +79,29 @@ func (c *DnsimpleApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.C
|
||||
continue
|
||||
}
|
||||
rec := &models.RecordConfig{
|
||||
NameFQDN: dnsutil.AddOrigin(r.Name, dc.Name),
|
||||
Type: r.Type,
|
||||
Target: r.Content,
|
||||
TTL: uint32(r.TTL),
|
||||
MxPreference: uint16(r.Priority),
|
||||
Original: r,
|
||||
TTL: uint32(r.TTL),
|
||||
Original: r,
|
||||
}
|
||||
if r.Type == "CAA" || r.Type == "SRV" {
|
||||
rec.CombinedTarget = true
|
||||
rec.SetLabel(r.Name, dc.Name)
|
||||
switch rtype := r.Type; rtype {
|
||||
case "MX":
|
||||
if err := rec.SetTargetMX(uint16(r.Priority), r.Content); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable record received from dnsimple"))
|
||||
}
|
||||
default:
|
||||
if err := rec.PopulateFromString(r.Type, r.Content, dc.Name); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable record received from dnsimple"))
|
||||
}
|
||||
}
|
||||
actual = append(actual, rec)
|
||||
}
|
||||
removeOtherNS(dc)
|
||||
dc.Filter(func(r *models.RecordConfig) bool {
|
||||
if r.Type == "CAA" || r.Type == "SRV" {
|
||||
r.MergeToTarget()
|
||||
}
|
||||
return true
|
||||
})
|
||||
// dc.Filter(func(r *models.RecordConfig) bool {
|
||||
// if r.Type == "CAA" || r.Type == "SRV" {
|
||||
// r.MergeToTarget()
|
||||
// }
|
||||
// return true
|
||||
// })
|
||||
|
||||
// Normalize
|
||||
models.PostProcessRecords(actual)
|
||||
@@ -276,7 +280,7 @@ func (c *DnsimpleApi) createRecordFunc(rc *models.RecordConfig, domainName strin
|
||||
record := dnsimpleapi.ZoneRecord{
|
||||
Name: dnsutil.TrimDomainName(rc.NameFQDN, domainName),
|
||||
Type: rc.Type,
|
||||
Content: rc.Target,
|
||||
Content: rc.GetTargetCombined(),
|
||||
TTL: int(rc.TTL),
|
||||
Priority: int(rc.MxPreference),
|
||||
}
|
||||
@@ -322,7 +326,7 @@ func (c *DnsimpleApi) updateRecordFunc(old *dnsimpleapi.ZoneRecord, rc *models.R
|
||||
record := dnsimpleapi.ZoneRecord{
|
||||
Name: dnsutil.TrimDomainName(rc.NameFQDN, domainName),
|
||||
Type: rc.Type,
|
||||
Content: rc.Target,
|
||||
Content: rc.GetTargetCombined(),
|
||||
TTL: int(rc.TTL),
|
||||
Priority: int(rc.MxPreference),
|
||||
}
|
||||
@@ -367,10 +371,10 @@ func removeOtherNS(dc *models.DomainConfig) {
|
||||
for _, rec := range dc.Records {
|
||||
if rec.Type == "NS" {
|
||||
// apex NS inside dnsimple are expected.
|
||||
if rec.NameFQDN == dc.Name && strings.HasSuffix(rec.Target, ".dnsimple.com.") {
|
||||
if rec.NameFQDN == dc.Name && strings.HasSuffix(rec.GetTargetField(), ".dnsimple.com.") {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Warning: dnsimple.com does not allow NS records to be modified. %s will not be added.\n", rec.Target)
|
||||
fmt.Printf("Warning: dnsimple.com does not allow NS records to be modified. %s will not be added.\n", rec.GetTargetField())
|
||||
continue
|
||||
}
|
||||
newList = append(newList, rec)
|
||||
|
||||
@@ -79,9 +79,6 @@ func (c *GandiApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||
// GetDomainCorrections returns a list of corrections recommended for this domain.
|
||||
func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
dc.Punycode()
|
||||
dc.CombineSRVs()
|
||||
dc.CombineCAAs()
|
||||
dc.CombineMXs()
|
||||
domaininfo, err := c.getDomainInfo(dc.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -95,7 +92,7 @@ func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Corr
|
||||
recordsToKeep := make([]*models.RecordConfig, 0, len(dc.Records))
|
||||
for _, rec := range dc.Records {
|
||||
if rec.TTL < 300 {
|
||||
log.Printf("WARNING: Gandi does not support ttls < 300. %s will not be set to %d.", rec.NameFQDN, rec.TTL)
|
||||
log.Printf("WARNING: Gandi does not support ttls < 300. Setting %s from %d to 300", rec.GetLabelFQDN(), rec.TTL)
|
||||
rec.TTL = 300
|
||||
}
|
||||
if rec.TTL > 2592000 {
|
||||
@@ -104,7 +101,7 @@ func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Corr
|
||||
if rec.Type == "TXT" {
|
||||
rec.Target = "\"" + rec.Target + "\"" // FIXME(tlim): Should do proper quoting.
|
||||
}
|
||||
if rec.Type == "NS" && rec.Name == "@" {
|
||||
if rec.Type == "NS" && rec.GetLabel() == "@" {
|
||||
if !strings.HasSuffix(rec.Target, ".gandi.net.") {
|
||||
log.Printf("WARNING: Gandi does not support changing apex NS records. %s will not be added.", rec.Target)
|
||||
}
|
||||
@@ -112,8 +109,8 @@ func (c *GandiApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Corr
|
||||
}
|
||||
rs := gandirecord.RecordSet{
|
||||
"type": rec.Type,
|
||||
"name": rec.Name,
|
||||
"value": rec.Target,
|
||||
"name": rec.GetLabel(),
|
||||
"value": rec.GetTargetCombined(),
|
||||
"ttl": rec.TTL,
|
||||
}
|
||||
expectedRecordSets = append(expectedRecordSets, rs)
|
||||
|
||||
@@ -3,6 +3,7 @@ package gandi
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
gandiclient "github.com/prasmussen/gandi-api/client"
|
||||
gandidomain "github.com/prasmussen/gandi-api/domain"
|
||||
gandinameservers "github.com/prasmussen/gandi-api/domain/nameservers"
|
||||
@@ -12,7 +13,6 @@ import (
|
||||
gandioperation "github.com/prasmussen/gandi-api/operation"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
)
|
||||
|
||||
// fetchDomainList gets list of domains for account. Cache ids for easy lookup.
|
||||
@@ -58,11 +58,32 @@ func (c *GandiApi) getZoneRecords(zoneid int64, origin string) ([]*models.Record
|
||||
}
|
||||
rcs := make([]*models.RecordConfig, 0, len(recs))
|
||||
for _, r := range recs {
|
||||
rcs = append(rcs, convert(r, origin))
|
||||
rcs = append(rcs, nativeToRecord(r, origin))
|
||||
}
|
||||
return rcs, nil
|
||||
}
|
||||
|
||||
// convert takes a DNS record from Gandi and returns our native RecordConfig format.
|
||||
func nativeToRecord(r *gandirecord.RecordInfo, origin string) *models.RecordConfig {
|
||||
|
||||
rc := &models.RecordConfig{
|
||||
//NameFQDN: dnsutil.AddOrigin(r.Name, origin),
|
||||
//Name: r.Name,
|
||||
//Type: r.Type,
|
||||
TTL: uint32(r.Ttl),
|
||||
Original: r,
|
||||
//Target: r.Value,
|
||||
}
|
||||
rc.SetLabel(r.Name, origin)
|
||||
switch rtype := r.Type; rtype {
|
||||
default: // "A", "AAAA", "CAA", "NS", "CNAME", "MX", "PTR", "SRV", "TXT"
|
||||
if err := rc.PopulateFromString(rtype, r.Value, origin); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable record received from gandi"))
|
||||
}
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
// listZones retrieves the list of zones.
|
||||
func (c *GandiApi) listZones() ([]*gandizone.ZoneInfoBase, error) {
|
||||
gc := gandiclient.New(c.ApiKey, gandiclient.Production)
|
||||
@@ -150,7 +171,6 @@ func (c *GandiApi) createGandiZone(domainname string, zoneID int64, records []ga
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// fmt.Println("ZONEINFO:", zoneinfo)
|
||||
zoneID, err = c.getEditableZone(domainname, zoneinfo)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -180,44 +200,3 @@ func (c *GandiApi) createGandiZone(domainname string, zoneID int64, records []ga
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// convert takes a DNS record from Gandi and returns our native RecordConfig format.
|
||||
func convert(r *gandirecord.RecordInfo, origin string) *models.RecordConfig {
|
||||
rc := &models.RecordConfig{
|
||||
NameFQDN: dnsutil.AddOrigin(r.Name, origin),
|
||||
Name: r.Name,
|
||||
Type: r.Type,
|
||||
Original: r,
|
||||
Target: r.Value,
|
||||
TTL: uint32(r.Ttl),
|
||||
}
|
||||
switch r.Type {
|
||||
case "A", "AAAA", "NS", "CNAME", "PTR":
|
||||
// no-op
|
||||
case "TXT":
|
||||
rc.SetTxtParse(r.Value)
|
||||
case "CAA":
|
||||
var err error
|
||||
rc.CaaTag, rc.CaaFlag, rc.Target, err = models.SplitCombinedCaaValue(r.Value)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("gandi.convert bad caa value format: %#v (%s)", r.Value, err))
|
||||
}
|
||||
case "SRV":
|
||||
var err error
|
||||
rc.SrvPriority, rc.SrvWeight, rc.SrvPort, rc.Target, err = models.SplitCombinedSrvValue(r.Value)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("gandi.convert bad srv value format: %#v (%s)", r.Value, err))
|
||||
}
|
||||
case "MX":
|
||||
var err error
|
||||
rc.MxPreference, rc.Target, err = models.SplitCombinedMxValue(r.Value)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("gandi.convert bad mx value format: %#v", r.Value))
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("gandi.convert unimplemented rtype %v", r.Type))
|
||||
// We panic so that we quickly find any switch statements
|
||||
// that have not been updated for a new RR type.
|
||||
}
|
||||
return rc
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"github.com/StackExchange/dnscontrol/providers"
|
||||
"github.com/StackExchange/dnscontrol/providers/diff"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var features = providers.DocumentationNotes{
|
||||
@@ -118,27 +119,12 @@ func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correc
|
||||
existingRecords := []*models.RecordConfig{}
|
||||
oldRRs := map[key]*dns.ResourceRecordSet{}
|
||||
for _, set := range rrs {
|
||||
nameWithoutDot := set.Name
|
||||
if strings.HasSuffix(nameWithoutDot, ".") {
|
||||
nameWithoutDot = nameWithoutDot[:len(nameWithoutDot)-1]
|
||||
}
|
||||
oldRRs[keyFor(set)] = set
|
||||
for _, rec := range set.Rrdatas {
|
||||
r := &models.RecordConfig{
|
||||
NameFQDN: nameWithoutDot,
|
||||
Type: set.Type,
|
||||
Target: rec,
|
||||
TTL: uint32(set.Ttl),
|
||||
CombinedTarget: true,
|
||||
}
|
||||
existingRecords = append(existingRecords, r)
|
||||
existingRecords = append(existingRecords, nativeToRecord(set, rec, dc.Name))
|
||||
}
|
||||
}
|
||||
|
||||
for _, want := range dc.Records {
|
||||
want.MergeToTarget()
|
||||
}
|
||||
|
||||
// Normalize
|
||||
models.PostProcessRecords(existingRecords)
|
||||
|
||||
@@ -176,7 +162,7 @@ func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correc
|
||||
}
|
||||
for _, r := range dc.Records {
|
||||
if keyForRec(r) == ck {
|
||||
newRRs.Rrdatas = append(newRRs.Rrdatas, r.Target)
|
||||
newRRs.Rrdatas = append(newRRs.Rrdatas, r.GetTargetCombined())
|
||||
newRRs.Ttl = int64(r.TTL)
|
||||
}
|
||||
}
|
||||
@@ -195,6 +181,16 @@ func (g *gcloud) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correc
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func nativeToRecord(set *dns.ResourceRecordSet, rec, origin string) *models.RecordConfig {
|
||||
r := &models.RecordConfig{}
|
||||
r.SetLabelFromFQDN(set.Name, origin)
|
||||
r.TTL = uint32(set.Ttl)
|
||||
if err := r.PopulateFromString(set.Type, rec, origin); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable record received from GCLOUD"))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (g *gcloud) getRecords(domain string) ([]*dns.ResourceRecordSet, string, error) {
|
||||
zone, err := g.getZone(domain)
|
||||
if err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
const defaultAPIBase = "api.name.com"
|
||||
|
||||
// NameCom describes a connection to the NDC API.
|
||||
type NameCom struct {
|
||||
APIUrl string `json:"apiurl"`
|
||||
APIUser string `json:"apiuser"`
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
var nsRegex = regexp.MustCompile(`ns([1-4])[a-z]{3}\.name\.com`)
|
||||
|
||||
// GetNameservers gets the nameservers set on a domain.
|
||||
func (n *NameCom) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||
// This is an interesting edge case. Name.com expects you to SET the nameservers to ns[1-4].name.com,
|
||||
// but it will internally set it to ns1xyz.name.com, where xyz is a uniqueish 3 letters.
|
||||
@@ -44,6 +45,7 @@ func (n *NameCom) getNameserversRaw(domain string) ([]string, error) {
|
||||
return response.Nameservers, nil
|
||||
}
|
||||
|
||||
// GetRegistrarCorrections gathers corrections that would being n to match dc.
|
||||
func (n *NameCom) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
nss, err := n.getNameserversRaw(dc.Name)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,10 +3,10 @@ package namedotcom
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/namedotcom/go/namecom"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
|
||||
@@ -21,6 +21,7 @@ var defaultNameservers = []*models.Nameserver{
|
||||
{Name: "ns4.name.com"},
|
||||
}
|
||||
|
||||
// GetDomainCorrections gathers correctios that would bring n to match dc.
|
||||
func (n *NameCom) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
dc.Punycode()
|
||||
records, err := n.getRecords(dc.Name)
|
||||
@@ -29,7 +30,7 @@ func (n *NameCom) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Corre
|
||||
}
|
||||
actual := make([]*models.RecordConfig, len(records))
|
||||
for i, r := range records {
|
||||
actual[i] = toRecord(r)
|
||||
actual[i] = toRecord(r, dc.Name)
|
||||
}
|
||||
|
||||
for _, rec := range dc.Records {
|
||||
@@ -83,43 +84,32 @@ func checkNSModifications(dc *models.DomainConfig) {
|
||||
dc.Records = newList
|
||||
}
|
||||
|
||||
// finds a string surrounded by quotes that might contain an escaped quote charactor.
|
||||
var quotedStringRegexp = regexp.MustCompile("\"((?:[^\"\\\\]|\\\\.)*)\"")
|
||||
|
||||
func toRecord(r *namecom.Record) *models.RecordConfig {
|
||||
func toRecord(r *namecom.Record, origin string) *models.RecordConfig {
|
||||
rc := &models.RecordConfig{
|
||||
NameFQDN: strings.TrimSuffix(r.Fqdn, "."),
|
||||
Type: r.Type,
|
||||
Target: r.Answer,
|
||||
TTL: r.TTL,
|
||||
Original: r,
|
||||
}
|
||||
switch r.Type { // #rtype_variations
|
||||
case "A", "AAAA", "ANAME", "CNAME", "NS":
|
||||
// nothing additional.
|
||||
if !strings.HasSuffix(r.Fqdn, ".") {
|
||||
panic(errors.Errorf("namedotcom suddenly changed protocol. Bailing. (%v)", r.Fqdn))
|
||||
}
|
||||
fqdn := r.Fqdn[:len(r.Fqdn)-1]
|
||||
rc.SetLabelFromFQDN(fqdn, origin)
|
||||
switch rtype := r.Type; rtype { // #rtype_variations
|
||||
case "TXT":
|
||||
if r.Answer[0] == '"' && r.Answer[len(r.Answer)-1] == '"' {
|
||||
txtStrings := []string{}
|
||||
for _, t := range quotedStringRegexp.FindAllStringSubmatch(r.Answer, -1) {
|
||||
txtStrings = append(txtStrings, t[1])
|
||||
}
|
||||
rc.SetTxts(txtStrings)
|
||||
}
|
||||
rc.SetTargetTXTs(decodeTxt(r.Answer))
|
||||
case "MX":
|
||||
rc.MxPreference = uint16(r.Priority)
|
||||
if err := rc.SetTargetMX(uint16(r.Priority), r.Answer); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable MX record received from ndc"))
|
||||
}
|
||||
case "SRV":
|
||||
parts := strings.Split(r.Answer, " ")
|
||||
weight, _ := strconv.ParseInt(parts[0], 10, 32)
|
||||
port, _ := strconv.ParseInt(parts[1], 10, 32)
|
||||
rc.SrvWeight = uint16(weight)
|
||||
rc.SrvPort = uint16(port)
|
||||
rc.SrvPriority = uint16(r.Priority)
|
||||
rc.MxPreference = 0
|
||||
rc.Target = parts[2] + "."
|
||||
default:
|
||||
panic(fmt.Sprintf("toRecord unimplemented rtype %v", r.Type))
|
||||
// We panic so that we quickly find any switch statements
|
||||
// that have not been updated for a new RR type.
|
||||
if err := rc.SetTargetSRVPriorityString(uint16(r.Priority), r.Answer+"."); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable SRV record received from ndc"))
|
||||
}
|
||||
default: // "A", "AAAA", "ANAME", "CNAME", "NS"
|
||||
if err := rc.PopulateFromString(rtype, r.Answer, r.Fqdn); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable record received from ndc"))
|
||||
}
|
||||
}
|
||||
return rc
|
||||
}
|
||||
@@ -163,17 +153,11 @@ func (n *NameCom) createRecord(rc *models.RecordConfig, domain string) error {
|
||||
TTL: rc.TTL,
|
||||
Priority: uint32(rc.MxPreference),
|
||||
}
|
||||
|
||||
switch rc.Type { // #rtype_variations
|
||||
case "A", "AAAA", "ANAME", "CNAME", "MX", "NS":
|
||||
// nothing
|
||||
case "TXT":
|
||||
if len(rc.TxtStrings) > 1 {
|
||||
record.Answer = ""
|
||||
for _, t := range rc.TxtStrings {
|
||||
record.Answer += "\"" + strings.Replace(t, "\"", "\\\"", -1) + "\""
|
||||
}
|
||||
}
|
||||
record.Answer = encodeTxt(rc.TxtStrings)
|
||||
case "SRV":
|
||||
record.Answer = fmt.Sprintf("%d %d %v", rc.SrvWeight, rc.SrvPort, rc.Target)
|
||||
record.Priority = uint32(rc.SrvPriority)
|
||||
@@ -186,6 +170,37 @@ func (n *NameCom) createRecord(rc *models.RecordConfig, domain string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// makeTxt encodes TxtStrings for sending in the CREATE/MODIFY API:
|
||||
func encodeTxt(txts []string) string {
|
||||
ans := txts[0]
|
||||
|
||||
if len(txts) > 1 {
|
||||
ans = ""
|
||||
for _, t := range txts {
|
||||
ans += `"` + strings.Replace(t, `"`, `\"`, -1) + `"`
|
||||
}
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
// finds a string surrounded by quotes that might contain an escaped quote charactor.
|
||||
var quotedStringRegexp = regexp.MustCompile(`"((?:[^"\\]|\\.)*)"`)
|
||||
|
||||
// decodeTxt decodes the TXT record as received from name.com and
|
||||
// returns the list of strings.
|
||||
func decodeTxt(s string) []string {
|
||||
|
||||
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
|
||||
txtStrings := []string{}
|
||||
for _, t := range quotedStringRegexp.FindAllStringSubmatch(s, -1) {
|
||||
txtString := strings.Replace(t[1], `\"`, `"`, -1)
|
||||
txtStrings = append(txtStrings, txtString)
|
||||
}
|
||||
return txtStrings
|
||||
}
|
||||
return []string{s}
|
||||
}
|
||||
|
||||
func (n *NameCom) deleteRecord(id int32, domain string) error {
|
||||
request := &namecom.DeleteRecordRequest{
|
||||
DomainName: domain,
|
||||
|
||||
51
providers/namedotcom/records_test.go
Normal file
51
providers/namedotcom/records_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package namedotcom
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var txtData = []struct {
|
||||
decoded []string
|
||||
encoded string
|
||||
}{
|
||||
{[]string{`simple`}, `simple`},
|
||||
{[]string{`changed`}, `changed`},
|
||||
{[]string{`with spaces`}, `with spaces`},
|
||||
{[]string{`with whitespace`}, `with whitespace`},
|
||||
{[]string{"one", "two"}, `"one""two"`},
|
||||
{[]string{"eh", "bee", "cee"}, `"eh""bee""cee"`},
|
||||
{[]string{"o\"ne", "tw\"o"}, `"o\"ne""tw\"o"`},
|
||||
{[]string{"dimple"}, `dimple`},
|
||||
{[]string{"fun", "two"}, `"fun""two"`},
|
||||
{[]string{"eh", "bzz", "cee"}, `"eh""bzz""cee"`},
|
||||
}
|
||||
|
||||
func TestEncodeTxt(t *testing.T) {
|
||||
// Test encoded the lists of strings into a string:
|
||||
for i, test := range txtData {
|
||||
enc := encodeTxt(test.decoded)
|
||||
if enc != test.encoded {
|
||||
t.Errorf("%v: txt\n data: []string{%v}\nexpected: %s\n got: %s",
|
||||
i, "`"+strings.Join(test.decoded, "`, `")+"`", test.encoded, enc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeTxt(t *testing.T) {
|
||||
// Test decoded a string into the list of strings:
|
||||
for i, test := range txtData {
|
||||
data := test.encoded
|
||||
got := decodeTxt(data)
|
||||
wanted := test.decoded
|
||||
if len(got) != len(wanted) {
|
||||
t.Errorf("%v: txt\n decode: %v\nexpected: `%v`\n got: `%v`\n", i, data, strings.Join(wanted, "`, `"), strings.Join(got, "`, `"))
|
||||
} else {
|
||||
for j := range got {
|
||||
if got[j] != wanted[j] {
|
||||
t.Errorf("%v: txt\n decode: %v\nexpected: `%v`\n got: `%v`\n", i, data, strings.Join(wanted, "`, `"), strings.Join(got, "`, `"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func (n *nsone) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||
|
||||
func (n *nsone) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
dc.Punycode()
|
||||
dc.CombineMXs()
|
||||
//dc.CombineMXs()
|
||||
z, _, err := n.Zones.Get(dc.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -120,7 +120,7 @@ func (n *nsone) modify(recs models.Records, domain string) error {
|
||||
func buildRecord(recs models.Records, domain string, id string) *dns.Record {
|
||||
r := recs[0]
|
||||
rec := &dns.Record{
|
||||
Domain: r.NameFQDN,
|
||||
Domain: r.GetLabelFQDN(),
|
||||
Type: r.Type,
|
||||
ID: id,
|
||||
TTL: int(r.TTL),
|
||||
@@ -128,11 +128,11 @@ func buildRecord(recs models.Records, domain string, id string) *dns.Record {
|
||||
}
|
||||
for _, r := range recs {
|
||||
if r.Type == "TXT" {
|
||||
rec.AddAnswer(&dns.Answer{Rdata: []string{r.Target}})
|
||||
rec.AddAnswer(&dns.Answer{Rdata: r.TxtStrings})
|
||||
} else if r.Type == "SRV" {
|
||||
rec.AddAnswer(&dns.Answer{Rdata: strings.Split(fmt.Sprintf("%d %d %d %v", r.SrvPriority, r.SrvWeight, r.SrvPort, r.Target), " ")})
|
||||
rec.AddAnswer(&dns.Answer{Rdata: strings.Split(fmt.Sprintf("%d %d %d %v", r.SrvPriority, r.SrvWeight, r.SrvPort, r.GetTargetField()), " ")})
|
||||
} else {
|
||||
rec.AddAnswer(&dns.Answer{Rdata: strings.Split(r.Target, " ")})
|
||||
rec.AddAnswer(&dns.Answer{Rdata: strings.Split(r.GetTargetField(), " ")})
|
||||
}
|
||||
}
|
||||
return rec
|
||||
@@ -142,15 +142,15 @@ func convert(zr *dns.ZoneRecord, domain string) ([]*models.RecordConfig, error)
|
||||
found := []*models.RecordConfig{}
|
||||
for _, ans := range zr.ShortAns {
|
||||
rec := &models.RecordConfig{
|
||||
NameFQDN: zr.Domain,
|
||||
Name: dnsutil.TrimDomainName(zr.Domain, domain),
|
||||
TTL: uint32(zr.TTL),
|
||||
Target: ans,
|
||||
Original: zr,
|
||||
Type: zr.Type,
|
||||
}
|
||||
if zr.Type == "MX" || zr.Type == "SRV" {
|
||||
rec.CombinedTarget = true
|
||||
rec.SetLabelFromFQDN(zr.Domain, domain)
|
||||
switch rtype := zr.Type; rtype {
|
||||
default:
|
||||
if err := rec.PopulateFromString(rtype, ans, domain); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable record received from ns1"))
|
||||
}
|
||||
}
|
||||
found = append(found, rec)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/StackExchange/dnscontrol/models"
|
||||
"github.com/StackExchange/dnscontrol/providers"
|
||||
"github.com/StackExchange/dnscontrol/providers/diff"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/xlucas/go-ovh/ovh"
|
||||
)
|
||||
@@ -83,7 +82,7 @@ func (e errNoExist) Error() string {
|
||||
|
||||
func (c *ovhProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
dc.Punycode()
|
||||
dc.CombineMXs()
|
||||
//dc.CombineMXs()
|
||||
|
||||
if !c.zones[dc.Name] {
|
||||
return nil, errNoExist{dc.Name}
|
||||
@@ -96,34 +95,10 @@ func (c *ovhProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.C
|
||||
|
||||
var actual []*models.RecordConfig
|
||||
for _, r := range records {
|
||||
if r.FieldType == "SOA" {
|
||||
continue
|
||||
rec := nativeToRecord(r, dc.Name)
|
||||
if rec != nil {
|
||||
actual = append(actual, rec)
|
||||
}
|
||||
|
||||
if r.SubDomain == "" {
|
||||
r.SubDomain = "@"
|
||||
}
|
||||
|
||||
// ovh uses a custom type for SPF and DKIM
|
||||
if r.FieldType == "SPF" || r.FieldType == "DKIM" {
|
||||
r.FieldType = "TXT"
|
||||
}
|
||||
|
||||
// ovh default is 3600
|
||||
if r.TTL == 0 {
|
||||
r.TTL = 3600
|
||||
}
|
||||
|
||||
rec := &models.RecordConfig{
|
||||
NameFQDN: dnsutil.AddOrigin(r.SubDomain, dc.Name),
|
||||
Name: r.SubDomain,
|
||||
Type: r.FieldType,
|
||||
Target: r.Target,
|
||||
TTL: uint32(r.TTL),
|
||||
CombinedTarget: true,
|
||||
Original: r,
|
||||
}
|
||||
actual = append(actual, rec)
|
||||
}
|
||||
|
||||
// Normalize
|
||||
@@ -171,6 +146,33 @@ func (c *ovhProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.C
|
||||
return corrections, nil
|
||||
}
|
||||
|
||||
func nativeToRecord(r *Record, origin string) *models.RecordConfig {
|
||||
if r.FieldType == "SOA" {
|
||||
return nil
|
||||
}
|
||||
rec := &models.RecordConfig{
|
||||
TTL: uint32(r.TTL),
|
||||
Original: r,
|
||||
}
|
||||
rtype := r.FieldType
|
||||
rec.SetLabel(r.SubDomain, origin)
|
||||
if err := rec.PopulateFromString(rtype, r.Target, origin); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable record received from ovh"))
|
||||
}
|
||||
|
||||
// ovh uses a custom type for SPF and DKIM
|
||||
if rtype == "SPF" || rtype == "DKIM" {
|
||||
rec.Type = "TXT"
|
||||
}
|
||||
|
||||
// ovh default is 3600
|
||||
if rec.TTL == 0 {
|
||||
rec.TTL = 3600
|
||||
}
|
||||
|
||||
return rec
|
||||
}
|
||||
|
||||
func (c *ovhProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
||||
|
||||
ns, err := c.fetchRegistrarNS(dc.Name)
|
||||
|
||||
@@ -114,7 +114,7 @@ func (c *ovhProvider) createRecordFunc(rc *models.RecordConfig, fqdn string) fun
|
||||
record := Record{
|
||||
SubDomain: dnsutil.TrimDomainName(rc.NameFQDN, fqdn),
|
||||
FieldType: rc.Type,
|
||||
Target: rc.Content(),
|
||||
Target: rc.GetTargetCombined(),
|
||||
TTL: rc.TTL,
|
||||
}
|
||||
if record.SubDomain == "@" {
|
||||
@@ -132,7 +132,7 @@ func (c *ovhProvider) updateRecordFunc(old *Record, rc *models.RecordConfig, fqd
|
||||
record := Record{
|
||||
SubDomain: dnsutil.TrimDomainName(rc.NameFQDN, fqdn),
|
||||
FieldType: rc.Type,
|
||||
Target: rc.Content(),
|
||||
Target: rc.GetTargetCombined(),
|
||||
TTL: rc.TTL,
|
||||
Zone: fqdn,
|
||||
ID: old.ID,
|
||||
|
||||
@@ -110,7 +110,7 @@ type key struct {
|
||||
}
|
||||
|
||||
func getKey(r *models.RecordConfig) key {
|
||||
return key{r.NameFQDN, r.Type}
|
||||
return key{r.GetLabelFQDN(), r.Type}
|
||||
}
|
||||
|
||||
type errNoExist struct {
|
||||
@@ -157,37 +157,9 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
||||
|
||||
var existingRecords = []*models.RecordConfig{}
|
||||
for _, set := range records {
|
||||
if set.AliasTarget == nil {
|
||||
for _, rec := range set.ResourceRecords {
|
||||
if *set.Type == "SOA" {
|
||||
continue
|
||||
}
|
||||
r := &models.RecordConfig{
|
||||
NameFQDN: unescape(set.Name),
|
||||
Type: *set.Type,
|
||||
Target: *rec.Value,
|
||||
TTL: uint32(*set.TTL),
|
||||
CombinedTarget: true,
|
||||
}
|
||||
existingRecords = append(existingRecords, r)
|
||||
}
|
||||
} else {
|
||||
r := &models.RecordConfig{
|
||||
NameFQDN: unescape(set.Name),
|
||||
Type: "R53_ALIAS",
|
||||
Target: aws.StringValue(set.AliasTarget.DNSName),
|
||||
CombinedTarget: true,
|
||||
TTL: 300,
|
||||
R53Alias: map[string]string{
|
||||
"type": *set.Type,
|
||||
"zone_id": *set.AliasTarget.HostedZoneId,
|
||||
},
|
||||
}
|
||||
existingRecords = append(existingRecords, r)
|
||||
}
|
||||
existingRecords = append(existingRecords, nativeToRecords(set, dc.Name)...)
|
||||
}
|
||||
for _, want := range dc.Records {
|
||||
want.MergeToTarget()
|
||||
// update zone_id to current zone.id if not specified by the user
|
||||
if want.Type == "R53_ALIAS" && want.R53Alias["zone_id"] == "" {
|
||||
want.R53Alias["zone_id"] = getZoneID(zone, want)
|
||||
@@ -255,7 +227,7 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
||||
Type: sPtr(k.Type),
|
||||
}
|
||||
for _, r := range recs {
|
||||
val := r.Target
|
||||
val := r.GetTargetCombined()
|
||||
if r.Type != "R53_ALIAS" {
|
||||
rr := &r53.ResourceRecord{
|
||||
Value: &val,
|
||||
@@ -303,6 +275,38 @@ func (r *route53Provider) GetDomainCorrections(dc *models.DomainConfig) ([]*mode
|
||||
|
||||
}
|
||||
|
||||
func nativeToRecords(set *r53.ResourceRecordSet, origin string) []*models.RecordConfig {
|
||||
results := []*models.RecordConfig{}
|
||||
if set.AliasTarget != nil {
|
||||
rc := &models.RecordConfig{
|
||||
Type: "R53_ALIAS",
|
||||
TTL: 300,
|
||||
R53Alias: map[string]string{
|
||||
"type": *set.Type,
|
||||
"zone_id": *set.AliasTarget.HostedZoneId,
|
||||
},
|
||||
}
|
||||
rc.SetLabelFromFQDN(unescape(set.Name), origin)
|
||||
rc.SetTarget(aws.StringValue(set.AliasTarget.DNSName))
|
||||
results = append(results, rc)
|
||||
} else {
|
||||
for _, rec := range set.ResourceRecords {
|
||||
switch rtype := *set.Type; rtype {
|
||||
case "SOA":
|
||||
continue
|
||||
default:
|
||||
rc := &models.RecordConfig{TTL: uint32(*set.TTL)}
|
||||
rc.SetLabelFromFQDN(unescape(set.Name), origin)
|
||||
if err := rc.PopulateFromString(*set.Type, *rec.Value, origin); err != nil {
|
||||
panic(errors.Wrap(err, "unparsable record received from R53"))
|
||||
}
|
||||
results = append(results, rc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func getAliasMap(r *models.RecordConfig) map[string]string {
|
||||
if r.Type != "R53_ALIAS" {
|
||||
return nil
|
||||
@@ -312,13 +316,14 @@ func getAliasMap(r *models.RecordConfig) map[string]string {
|
||||
|
||||
func aliasToRRSet(zone *r53.HostedZone, r *models.RecordConfig) *r53.ResourceRecordSet {
|
||||
rrset := &r53.ResourceRecordSet{
|
||||
Name: sPtr(r.NameFQDN),
|
||||
Name: sPtr(r.GetLabelFQDN()),
|
||||
Type: sPtr(r.R53Alias["type"]),
|
||||
}
|
||||
zoneID := getZoneID(zone, r)
|
||||
targetHealth := false
|
||||
target := r.GetTargetField()
|
||||
rrset.AliasTarget = &r53.AliasTarget{
|
||||
DNSName: &r.Target,
|
||||
DNSName: &target,
|
||||
HostedZoneId: aws.String(zoneID),
|
||||
EvaluateTargetHealth: &targetHealth,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package vultr
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
vultr "github.com/JamesClonk/vultr/lib"
|
||||
@@ -194,109 +193,54 @@ func (api *VultrApi) isDomainInAccount(domain string) (bool, error) {
|
||||
|
||||
// toRecordConfig converts a Vultr DNSRecord to a RecordConfig #rtype_variations
|
||||
func toRecordConfig(dc *models.DomainConfig, r *vultr.DNSRecord) (*models.RecordConfig, error) {
|
||||
// Turns r.Name into a FQDN
|
||||
// Vultr uses "" as the apex domain, instead of "@", and this handles it fine.
|
||||
name := dnsutil.AddOrigin(r.Name, dc.Name)
|
||||
|
||||
origin := dc.Name
|
||||
data := r.Data
|
||||
// Make target into a FQDN if it is a CNAME, NS, MX, or SRV
|
||||
if r.Type == "CNAME" || r.Type == "NS" || r.Type == "MX" {
|
||||
if !strings.HasSuffix(data, ".") {
|
||||
data = data + "."
|
||||
}
|
||||
data = dnsutil.AddOrigin(data, dc.Name)
|
||||
}
|
||||
// Remove quotes if it is a TXT
|
||||
if r.Type == "TXT" {
|
||||
if !strings.HasPrefix(data, `"`) || !strings.HasSuffix(data, `"`) {
|
||||
return nil, errors.New("Unexpected lack of quotes in TXT record from Vultr")
|
||||
}
|
||||
data = data[1 : len(data)-1]
|
||||
}
|
||||
|
||||
rc := &models.RecordConfig{
|
||||
NameFQDN: name,
|
||||
Type: r.Type,
|
||||
Target: data,
|
||||
TTL: uint32(r.TTL),
|
||||
Original: r,
|
||||
}
|
||||
rc.SetLabel(r.Name, dc.Name)
|
||||
|
||||
if r.Type == "MX" {
|
||||
rc.MxPreference = uint16(r.Priority)
|
||||
}
|
||||
|
||||
if r.Type == "SRV" {
|
||||
rc.SrvPriority = uint16(r.Priority)
|
||||
|
||||
// Vultr returns in the format "[weight] [port] [target]"
|
||||
splitData := strings.SplitN(rc.Target, " ", 3)
|
||||
if len(splitData) != 3 {
|
||||
return nil, errors.Errorf("Unexpected data for SRV record returned by Vultr")
|
||||
switch rtype := r.Type; rtype {
|
||||
case "CNAME", "NS":
|
||||
rc.Type = r.Type
|
||||
// Make target into a FQDN if it is a CNAME, NS, MX, or SRV
|
||||
if !strings.HasSuffix(data, ".") {
|
||||
data = data + "."
|
||||
}
|
||||
|
||||
weight, err := strconv.ParseUint(splitData[0], 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc.SrvWeight = uint16(weight)
|
||||
|
||||
port, err := strconv.ParseUint(splitData[1], 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc.SrvPort = uint16(port)
|
||||
|
||||
target := splitData[2]
|
||||
if !strings.HasSuffix(target, ".") {
|
||||
target = target + "."
|
||||
}
|
||||
rc.Target = dnsutil.AddOrigin(target, dc.Name)
|
||||
}
|
||||
|
||||
if r.Type == "CAA" {
|
||||
// FIXME(tlim): the AddOrigin() might be unneeded. Please test.
|
||||
return rc, rc.SetTarget(dnsutil.AddOrigin(data, origin))
|
||||
case "CAA":
|
||||
// Vultr returns in the format "[flag] [tag] [value]"
|
||||
// TODO(tal): I copied this code into models/dns.go. At this point
|
||||
// we can probably replace the code below with:
|
||||
// rc.CaaFlag, rc.CaaTag, rc.Target, err := models.SplitCombinedCaaValue(rc.Target)
|
||||
// return rc, err
|
||||
|
||||
splitData := strings.SplitN(rc.Target, " ", 3)
|
||||
if len(splitData) != 3 {
|
||||
return nil, errors.Errorf("Unexpected data for CAA record returned by Vultr")
|
||||
return rc, rc.SetTargetCAAString(data)
|
||||
case "MX":
|
||||
if !strings.HasSuffix(data, ".") {
|
||||
data = data + "."
|
||||
}
|
||||
|
||||
flag, err := strconv.ParseUint(splitData[0], 10, 8)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return rc, rc.SetTargetMX(uint16(r.Priority), data)
|
||||
case "SRV":
|
||||
// Vultr returns in the format "[weight] [port] [target]"
|
||||
return rc, rc.SetTargetSRVPriorityString(uint16(r.Priority), data)
|
||||
case "TXT":
|
||||
// Remove quotes if it is a TXT
|
||||
if !strings.HasPrefix(data, `"`) || !strings.HasSuffix(data, `"`) {
|
||||
return nil, errors.New("Unexpected lack of quotes in TXT record from Vultr")
|
||||
}
|
||||
rc.CaaFlag = uint8(flag)
|
||||
|
||||
rc.CaaTag = 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]
|
||||
}
|
||||
rc.Target = value
|
||||
return rc, rc.SetTargetTXT(data[1 : len(data)-1])
|
||||
default:
|
||||
return rc, rc.PopulateFromString(rtype, r.Data, origin)
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
// toVultrRecord converts a RecordConfig converted by toRecordConfig back to a Vultr DNSRecord #rtype_variations
|
||||
func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig) *vultr.DNSRecord {
|
||||
name := dnsutil.TrimDomainName(rc.NameFQDN, dc.Name)
|
||||
|
||||
// Vultr uses a blank string to represent the apex domain
|
||||
if name == "@" {
|
||||
name = ""
|
||||
}
|
||||
|
||||
data := rc.Target
|
||||
data := rc.GetTargetField()
|
||||
|
||||
// Vultr does not use a period suffix for the server for CNAME, NS, or MX
|
||||
if strings.HasSuffix(data, ".") {
|
||||
@@ -312,7 +256,6 @@ func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig) *vultr.DNSR
|
||||
if rc.Type == "MX" {
|
||||
priority = int(rc.MxPreference)
|
||||
}
|
||||
|
||||
if rc.Type == "SRV" {
|
||||
priority = int(rc.SrvPriority)
|
||||
}
|
||||
@@ -330,12 +273,11 @@ func toVultrRecord(dc *models.DomainConfig, rc *models.RecordConfig) *vultr.DNSR
|
||||
if strings.HasSuffix(target, ".") {
|
||||
target = target[:len(target)-1]
|
||||
}
|
||||
|
||||
r.Data = fmt.Sprintf("%v %v %s", rc.SrvWeight, rc.SrvPort, target)
|
||||
}
|
||||
|
||||
if rc.Type == "CAA" {
|
||||
r.Data = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.Target)
|
||||
r.Data = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, rc.GetTargetField())
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
Reference in New Issue
Block a user