github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/namedotcom/records.go (about)

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