github.com/karsthammer/dnscontrol@v0.2.8/providers/namedotcom/records.go (about)

     1  package namedotcom
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strings"
     7  
     8  	"github.com/namedotcom/go/namecom"
     9  	"github.com/pkg/errors"
    10  
    11  	"github.com/StackExchange/dnscontrol/models"
    12  	"github.com/StackExchange/dnscontrol/providers/diff"
    13  )
    14  
    15  var defaultNameservers = []*models.Nameserver{
    16  	{Name: "ns1.name.com"},
    17  	{Name: "ns2.name.com"},
    18  	{Name: "ns3.name.com"},
    19  	{Name: "ns4.name.com"},
    20  }
    21  
    22  // GetDomainCorrections gathers correctios that would bring n to match dc.
    23  func (n *NameCom) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
    24  	dc.Punycode()
    25  	records, err := n.getRecords(dc.Name)
    26  	if err != nil {
    27  		return nil, err
    28  	}
    29  	actual := make([]*models.RecordConfig, len(records))
    30  	for i, r := range records {
    31  		actual[i] = toRecord(r, dc.Name)
    32  	}
    33  
    34  	for _, rec := range dc.Records {
    35  		if rec.Type == "ALIAS" {
    36  			rec.Type = "ANAME"
    37  		}
    38  	}
    39  
    40  	checkNSModifications(dc)
    41  
    42  	// Normalize
    43  	models.PostProcessRecords(actual)
    44  
    45  	differ := diff.New(dc)
    46  	_, create, del, mod := differ.IncrementalDiff(actual)
    47  	corrections := []*models.Correction{}
    48  
    49  	for _, d := range del {
    50  		rec := d.Existing.Original.(*namecom.Record)
    51  		c := &models.Correction{Msg: d.String(), F: func() error { return n.deleteRecord(rec.ID, dc.Name) }}
    52  		corrections = append(corrections, c)
    53  	}
    54  	for _, cre := range create {
    55  		rec := cre.Desired
    56  		c := &models.Correction{Msg: cre.String(), F: func() error { return n.createRecord(rec, dc.Name) }}
    57  		corrections = append(corrections, c)
    58  	}
    59  	for _, chng := range mod {
    60  		old := chng.Existing.Original.(*namecom.Record)
    61  		new := chng.Desired
    62  		c := &models.Correction{Msg: chng.String(), F: func() error {
    63  			err := n.deleteRecord(old.ID, dc.Name)
    64  			if err != nil {
    65  				return err
    66  			}
    67  			return n.createRecord(new, dc.Name)
    68  		}}
    69  		corrections = append(corrections, c)
    70  	}
    71  	return corrections, nil
    72  }
    73  
    74  func checkNSModifications(dc *models.DomainConfig) {
    75  	newList := make([]*models.RecordConfig, 0, len(dc.Records))
    76  	for _, rec := range dc.Records {
    77  		if rec.Type == "NS" && rec.GetLabel() == "@" {
    78  			continue // Apex NS records are automatically created for the domain's nameservers and cannot be managed otherwise via the name.com API.
    79  		}
    80  		newList = append(newList, rec)
    81  	}
    82  	dc.Records = newList
    83  }
    84  
    85  func toRecord(r *namecom.Record, origin string) *models.RecordConfig {
    86  	rc := &models.RecordConfig{
    87  		Type:     r.Type,
    88  		TTL:      r.TTL,
    89  		Original: r,
    90  	}
    91  	if !strings.HasSuffix(r.Fqdn, ".") {
    92  		panic(errors.Errorf("namedotcom suddenly changed protocol. Bailing. (%v)", r.Fqdn))
    93  	}
    94  	fqdn := r.Fqdn[:len(r.Fqdn)-1]
    95  	rc.SetLabelFromFQDN(fqdn, origin)
    96  	switch rtype := r.Type; rtype { // #rtype_variations
    97  	case "TXT":
    98  		rc.SetTargetTXTs(decodeTxt(r.Answer))
    99  	case "MX":
   100  		if err := rc.SetTargetMX(uint16(r.Priority), r.Answer); err != nil {
   101  			panic(errors.Wrap(err, "unparsable MX record received from ndc"))
   102  		}
   103  	case "SRV":
   104  		if err := rc.SetTargetSRVPriorityString(uint16(r.Priority), r.Answer+"."); err != nil {
   105  			panic(errors.Wrap(err, "unparsable SRV record received from ndc"))
   106  		}
   107  	default: // "A", "AAAA", "ANAME", "CNAME", "NS"
   108  		if err := rc.PopulateFromString(rtype, r.Answer, r.Fqdn); err != nil {
   109  			panic(errors.Wrap(err, "unparsable record received from ndc"))
   110  		}
   111  	}
   112  	return rc
   113  }
   114  
   115  func (n *NameCom) getRecords(domain string) ([]*namecom.Record, error) {
   116  	var (
   117  		err      error
   118  		records  []*namecom.Record
   119  		response *namecom.ListRecordsResponse
   120  	)
   121  
   122  	request := &namecom.ListRecordsRequest{
   123  		DomainName: domain,
   124  		Page:       1,
   125  	}
   126  
   127  	for request.Page > 0 {
   128  		response, err = n.client.ListRecords(request)
   129  		if err != nil {
   130  			return nil, err
   131  		}
   132  
   133  		records = append(records, response.Records...)
   134  		request.Page = response.NextPage
   135  	}
   136  
   137  	for _, rc := range records {
   138  		if rc.Type == "CNAME" || rc.Type == "ANAME" || rc.Type == "MX" || rc.Type == "NS" {
   139  			rc.Answer = rc.Answer + "."
   140  		}
   141  	}
   142  	return records, nil
   143  }
   144  
   145  func (n *NameCom) createRecord(rc *models.RecordConfig, domain string) error {
   146  	record := &namecom.Record{
   147  		DomainName: domain,
   148  		Host:       rc.GetLabel(),
   149  		Type:       rc.Type,
   150  		Answer:     rc.GetTargetField(),
   151  		TTL:        rc.TTL,
   152  		Priority:   uint32(rc.MxPreference),
   153  	}
   154  	switch rc.Type { // #rtype_variations
   155  	case "A", "AAAA", "ANAME", "CNAME", "MX", "NS":
   156  		// nothing
   157  	case "TXT":
   158  		record.Answer = encodeTxt(rc.TxtStrings)
   159  	case "SRV":
   160  		record.Answer = fmt.Sprintf("%d %d %v", rc.SrvWeight, rc.SrvPort, rc.GetTargetField())
   161  		record.Priority = uint32(rc.SrvPriority)
   162  	default:
   163  		panic(fmt.Sprintf("createRecord rtype %v unimplemented", rc.Type))
   164  		// We panic so that we quickly find any switch statements
   165  		// that have not been updated for a new RR type.
   166  	}
   167  	_, err := n.client.CreateRecord(record)
   168  	return err
   169  }
   170  
   171  // makeTxt encodes TxtStrings for sending in the CREATE/MODIFY API:
   172  func encodeTxt(txts []string) string {
   173  	ans := txts[0]
   174  
   175  	if len(txts) > 1 {
   176  		ans = ""
   177  		for _, t := range txts {
   178  			ans += `"` + strings.Replace(t, `"`, `\"`, -1) + `"`
   179  		}
   180  	}
   181  	return ans
   182  }
   183  
   184  // finds a string surrounded by quotes that might contain an escaped quote charactor.
   185  var quotedStringRegexp = regexp.MustCompile(`"((?:[^"\\]|\\.)*)"`)
   186  
   187  // decodeTxt decodes the TXT record as received from name.com and
   188  // returns the list of strings.
   189  func decodeTxt(s string) []string {
   190  
   191  	if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
   192  		txtStrings := []string{}
   193  		for _, t := range quotedStringRegexp.FindAllStringSubmatch(s, -1) {
   194  			txtString := strings.Replace(t[1], `\"`, `"`, -1)
   195  			txtStrings = append(txtStrings, txtString)
   196  		}
   197  		return txtStrings
   198  	}
   199  	return []string{s}
   200  }
   201  
   202  func (n *NameCom) deleteRecord(id int32, domain string) error {
   203  	request := &namecom.DeleteRecordRequest{
   204  		DomainName: domain,
   205  		ID:         id,
   206  	}
   207  
   208  	_, err := n.client.DeleteRecord(request)
   209  	return err
   210  }