mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-11 01:47:53 +08:00
235 lines
6.4 KiB
Go
235 lines
6.4 KiB
Go
package namedotcom
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff2"
|
|
"github.com/namedotcom/go/namecom"
|
|
)
|
|
|
|
var defaultNameservers = []*models.Nameserver{
|
|
{Name: "ns1.name.com"},
|
|
{Name: "ns2.name.com"},
|
|
{Name: "ns3.name.com"},
|
|
{Name: "ns4.name.com"},
|
|
}
|
|
|
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
|
func (n *namedotcomProvider) GetZoneRecords(domain string) (models.Records, error) {
|
|
records, err := n.getRecords(domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
actual := make([]*models.RecordConfig, len(records))
|
|
for i, r := range records {
|
|
actual[i] = toRecord(r, domain)
|
|
}
|
|
|
|
return actual, nil
|
|
}
|
|
|
|
// GetDomainCorrections gathers correctios that would bring n to match dc.
|
|
func (n *namedotcomProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
dc.Punycode()
|
|
|
|
actual, err := n.GetZoneRecords(dc.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, rec := range dc.Records {
|
|
if rec.Type == "ALIAS" {
|
|
rec.Type = "ANAME"
|
|
}
|
|
}
|
|
|
|
checkNSModifications(dc)
|
|
|
|
// Normalize
|
|
models.PostProcessRecords(actual)
|
|
|
|
var corrections []*models.Correction
|
|
var create, del, mod diff.Changeset
|
|
if !diff2.EnableDiff2 {
|
|
differ := diff.New(dc)
|
|
_, create, del, mod, err = differ.IncrementalDiff(actual)
|
|
} else {
|
|
differ := diff.NewCompat(dc)
|
|
_, create, del, mod, err = differ.IncrementalDiff(actual)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, d := range del {
|
|
rec := d.Existing.Original.(*namecom.Record)
|
|
c := &models.Correction{Msg: d.String(), F: func() error { return n.deleteRecord(rec.ID, 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.(*namecom.Record)
|
|
new := chng.Desired
|
|
c := &models.Correction{Msg: chng.String(), F: func() error {
|
|
err := n.deleteRecord(old.ID, dc.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return n.createRecord(new, dc.Name)
|
|
}}
|
|
corrections = append(corrections, c)
|
|
}
|
|
|
|
return corrections, nil
|
|
}
|
|
|
|
func checkNSModifications(dc *models.DomainConfig) {
|
|
newList := make([]*models.RecordConfig, 0, len(dc.Records))
|
|
for _, rec := range dc.Records {
|
|
if rec.Type == "NS" && rec.GetLabel() == "@" {
|
|
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 toRecord(r *namecom.Record, origin string) *models.RecordConfig {
|
|
rc := &models.RecordConfig{
|
|
Type: r.Type,
|
|
TTL: r.TTL,
|
|
Original: r,
|
|
}
|
|
if !strings.HasSuffix(r.Fqdn, ".") {
|
|
panic(fmt.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":
|
|
rc.SetTargetTXTs(decodeTxt(r.Answer))
|
|
case "MX":
|
|
if err := rc.SetTargetMX(uint16(r.Priority), r.Answer); err != nil {
|
|
panic(fmt.Errorf("unparsable MX record received from ndc: %w", err))
|
|
}
|
|
case "SRV":
|
|
if err := rc.SetTargetSRVPriorityString(uint16(r.Priority), r.Answer+"."); err != nil {
|
|
panic(fmt.Errorf("unparsable SRV record received from ndc: %w", err))
|
|
}
|
|
default: // "A", "AAAA", "ANAME", "CNAME", "NS"
|
|
if err := rc.PopulateFromString(rtype, r.Answer, r.Fqdn); err != nil {
|
|
panic(fmt.Errorf("unparsable record received from ndc: %w", err))
|
|
}
|
|
}
|
|
return rc
|
|
}
|
|
|
|
func (n *namedotcomProvider) getRecords(domain string) ([]*namecom.Record, error) {
|
|
var (
|
|
err error
|
|
records []*namecom.Record
|
|
response *namecom.ListRecordsResponse
|
|
)
|
|
|
|
request := &namecom.ListRecordsRequest{
|
|
DomainName: domain,
|
|
Page: 1,
|
|
}
|
|
|
|
for request.Page > 0 {
|
|
response, err = n.client.ListRecords(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
records = append(records, response.Records...)
|
|
request.Page = response.NextPage
|
|
}
|
|
|
|
for _, rc := range records {
|
|
if rc.Type == "CNAME" || rc.Type == "ANAME" || rc.Type == "MX" || rc.Type == "NS" {
|
|
rc.Answer = rc.Answer + "."
|
|
}
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
func (n *namedotcomProvider) createRecord(rc *models.RecordConfig, domain string) error {
|
|
record := &namecom.Record{
|
|
DomainName: domain,
|
|
Host: rc.GetLabel(),
|
|
Type: rc.Type,
|
|
Answer: rc.GetTargetField(),
|
|
TTL: rc.TTL,
|
|
Priority: uint32(rc.MxPreference),
|
|
}
|
|
switch rc.Type { // #rtype_variations
|
|
case "A", "AAAA", "ANAME", "CNAME", "MX", "NS":
|
|
// nothing
|
|
case "TXT":
|
|
// record.Answer = encodeTxt(rc.TxtStrings)
|
|
case "SRV":
|
|
if rc.GetTargetField() == "." {
|
|
return errors.New("SRV records with empty targets are not supported (as of 2019-11-05, the API returns 'Parameter Value Error - Invalid Srv Format')")
|
|
}
|
|
record.Answer = fmt.Sprintf("%d %d %v", rc.SrvWeight, rc.SrvPort, rc.GetTargetField())
|
|
record.Priority = uint32(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.
|
|
}
|
|
_, err := n.client.CreateRecord(record)
|
|
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 character.
|
|
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 *namedotcomProvider) deleteRecord(id int32, domain string) error {
|
|
request := &namecom.DeleteRecordRequest{
|
|
DomainName: domain,
|
|
ID: id,
|
|
}
|
|
|
|
_, err := n.client.DeleteRecord(request)
|
|
return err
|
|
}
|