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 }