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) // Normalize models.Downcase(actual) 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() }