dnscontrol/providers/namedotcom/records.go
Pat Moroney 93764da4e3 Namecom punycode (#170)
* convert records to punycode before processing

* make IDN CNAME target use a real IDN TLD
2017-08-05 08:08:22 -07:00

206 lines
5.5 KiB
Go

package namedotcom
import (
"fmt"
"strconv"
"strings"
"github.com/miekg/dns/dnsutil"
"github.com/StackExchange/dnscontrol/models"
"github.com/StackExchange/dnscontrol/providers/diff"
)
var defaultNameservers = []*models.Nameserver{
{Name: "ns1.name.com"},
{Name: "ns2.name.com"},
{Name: "ns3.name.com"},
{Name: "ns4.name.com"},
}
func (n *nameDotCom) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc.Punycode()
records, err := n.getRecords(dc.Name)
if err != nil {
return nil, err
}
actual := make([]*models.RecordConfig, len(records))
for i, r := range records {
actual[i] = r.toRecord()
}
for _, rec := range dc.Records {
if rec.Type == "ALIAS" {
rec.Type = "ANAME"
}
}
checkNSModifications(dc)
differ := diff.New(dc)
_, create, del, mod := differ.IncrementalDiff(actual)
corrections := []*models.Correction{}
for _, d := range del {
rec := d.Existing.Original.(*nameComRecord)
c := &models.Correction{Msg: d.String(), F: func() error { return n.deleteRecord(rec.RecordID, dc.Name) }}
corrections = append(corrections, c)
}
for _, cre := range create {
rec := cre.Desired
c := &models.Correction{Msg: cre.String(), F: func() error { return n.createRecord(rec, dc.Name) }}
corrections = append(corrections, c)
}
for _, chng := range mod {
old := chng.Existing.Original.(*nameComRecord)
new := chng.Desired
c := &models.Correction{Msg: chng.String(), F: func() error {
err := n.deleteRecord(old.RecordID, dc.Name)
if err != nil {
return err
}
return n.createRecord(new, dc.Name)
}}
corrections = append(corrections, c)
}
return corrections, nil
}
func (n *nameDotCom) apiGetRecords(domain string) string {
return fmt.Sprintf("%s/dns/list/%s", n.APIUrl, domain)
}
func (n *nameDotCom) apiCreateRecord(domain string) string {
return fmt.Sprintf("%s/dns/create/%s", n.APIUrl, domain)
}
func (n *nameDotCom) apiDeleteRecord(domain string) string {
return fmt.Sprintf("%s/dns/delete/%s", n.APIUrl, domain)
}
type nameComRecord struct {
RecordID string `json:"record_id"`
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
TTL string `json:"ttl"`
Priority string `json:"priority"`
}
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 {
continue // Apex NS records are automatically created for the domain's nameservers and cannot be managed otherwise via the name.com API.
}
newList = append(newList, rec)
}
dc.Records = newList
}
func (r *nameComRecord) toRecord() *models.RecordConfig {
ttl, _ := strconv.ParseUint(r.TTL, 10, 32)
prio, _ := strconv.ParseUint(r.Priority, 10, 16)
rc := &models.RecordConfig{
NameFQDN: r.Name,
Type: r.Type,
Target: r.Content,
TTL: uint32(ttl),
Original: r,
}
switch r.Type { // #rtype_variations
case "A", "AAAA", "ANAME", "CNAME", "NS", "TXT":
// nothing additional.
case "MX":
rc.MxPreference = uint16(prio)
case "SRV":
parts := strings.Split(r.Content, " ")
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(prio)
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.
}
return rc
}
type listRecordsResponse struct {
*apiResult
Records []*nameComRecord `json:"records"`
}
func (n *nameDotCom) getRecords(domain string) ([]*nameComRecord, error) {
result := &listRecordsResponse{}
err := n.get(n.apiGetRecords(domain), result)
if err != nil {
return nil, err
}
if err = result.getErr(); err != nil {
return nil, err
}
for _, rc := range result.Records {
if rc.Type == "CNAME" || rc.Type == "ANAME" || rc.Type == "MX" || rc.Type == "NS" {
rc.Content = rc.Content + "."
}
}
return result.Records, nil
}
func (n *nameDotCom) createRecord(rc *models.RecordConfig, domain string) error {
target := rc.Target
if rc.Type == "CNAME" || rc.Type == "ANAME" || rc.Type == "MX" || rc.Type == "NS" {
if target[len(target)-1] == '.' {
target = target[:len(target)-1]
} else {
return fmt.Errorf("Unexpected. CNAME/MX/NS target did not end with dot.\n")
}
}
dat := struct {
Hostname string `json:"hostname"`
Type string `json:"type"`
Content string `json:"content"`
TTL uint32 `json:"ttl,omitempty"`
Priority uint16 `json:"priority,omitempty"`
}{
Hostname: dnsutil.TrimDomainName(rc.NameFQDN, domain),
Type: rc.Type,
Content: target,
TTL: rc.TTL,
Priority: rc.MxPreference,
}
if dat.Hostname == "@" {
dat.Hostname = ""
}
switch rc.Type { // #rtype_variations
case "A", "AAAA", "ANAME", "CNAME", "MX", "NS", "TXT":
// nothing
case "SRV":
dat.Content = fmt.Sprintf("%d %d %v", rc.SrvWeight, rc.SrvPort, rc.Target)
dat.Priority = rc.SrvPriority
default:
panic(fmt.Sprintf("createRecord 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.
}
resp, err := n.post(n.apiCreateRecord(domain), dat)
if err != nil {
return err
}
return resp.getErr()
}
func (n *nameDotCom) deleteRecord(id, domain string) error {
dat := struct {
ID string `json:"record_id"`
}{id}
resp, err := n.post(n.apiDeleteRecord(domain), dat)
if err != nil {
return err
}
return resp.getErr()
}