github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/hexonet/records.go (about) 1 package hexonet 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "regexp" 8 "strconv" 9 "strings" 10 11 "github.com/StackExchange/dnscontrol/v2/models" 12 "github.com/StackExchange/dnscontrol/v2/providers/diff" 13 ) 14 15 // HXRecord covers an individual DNS resource record. 16 type HXRecord struct { 17 // Raw api value of that RR 18 Raw string 19 // DomainName is the zone that the record belongs to. 20 DomainName string 21 // Host is the hostname relative to the zone: e.g. for a record for blog.example.org, domain would be "example.org" and host would be "blog". 22 // An apex record would be specified by either an empty host "" or "@". 23 // A SRV record would be specified by "_{service}._{protocal}.{host}": e.g. "_sip._tcp.phone" for _sip._tcp.phone.example.org. 24 Host string 25 // FQDN is the Fully Qualified Domain Name. It is the combination of the host and the domain name. It always ends in a ".". FQDN is ignored in CreateRecord, specify via the Host field instead. 26 Fqdn string 27 // Type is one of the following: A, AAAA, ANAME, CNAME, MX, NS, SRV, or TXT. 28 Type string 29 // Answer is either the IP address for A or AAAA records; the target for ANAME, CNAME, MX, or NS records; the text for TXT records. 30 // For SRV records, answer has the following format: "{weight} {port} {target}" e.g. "1 5061 sip.example.org". 31 Answer string 32 // TTL is the time this record can be cached for in seconds. 33 TTL uint32 34 // Priority is only required for MX and SRV records, it is ignored for all others. 35 Priority uint32 36 } 37 38 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. 39 func (n *HXClient) GetZoneRecords(domain string) (models.Records, error) { 40 return nil, fmt.Errorf("not implemented") 41 // This enables the get-zones subcommand. 42 // Implement this by extracting the code from GetDomainCorrections into 43 // a single function. For most providers this should be relatively easy. 44 } 45 46 // GetDomainCorrections gathers correctios that would bring n to match dc. 47 func (n *HXClient) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 48 dc.Punycode() 49 records, err := n.getRecords(dc.Name) 50 if err != nil { 51 return nil, err 52 } 53 actual := make([]*models.RecordConfig, len(records)) 54 for i, r := range records { 55 actual[i] = toRecord(r, dc.Name) 56 } 57 58 for _, rec := range dc.Records { 59 if rec.Type == "ALIAS" { 60 return nil, fmt.Errorf("We support realtime ALIAS RR over our X-DNS service, please get in touch with us") 61 } 62 } 63 64 //checkNSModifications(dc) 65 66 // Normalize 67 models.PostProcessRecords(actual) 68 69 differ := diff.New(dc) 70 _, create, del, mod := differ.IncrementalDiff(actual) 71 corrections := []*models.Correction{} 72 73 buf := &bytes.Buffer{} 74 // Print a list of changes. Generate an actual change that is the zone 75 changes := false 76 params := map[string]string{} 77 delrridx := 0 78 addrridx := 0 79 for _, cre := range create { 80 changes = true 81 fmt.Fprintln(buf, cre) 82 rec := cre.Desired 83 recordString, err := n.createRecordString(rec, dc.Name) 84 if err != nil { 85 return corrections, err 86 } 87 params[fmt.Sprintf("ADDRR%d", addrridx)] = recordString 88 addrridx++ 89 } 90 for _, d := range del { 91 changes = true 92 fmt.Fprintln(buf, d) 93 rec := d.Existing.Original.(*HXRecord) 94 params[fmt.Sprintf("DELRR%d", delrridx)] = n.deleteRecordString(rec, dc.Name) 95 delrridx++ 96 } 97 for _, chng := range mod { 98 changes = true 99 fmt.Fprintln(buf, chng) 100 old := chng.Existing.Original.(*HXRecord) 101 new := chng.Desired 102 params[fmt.Sprintf("DELRR%d", delrridx)] = n.deleteRecordString(old, dc.Name) 103 newRecordString, err := n.createRecordString(new, dc.Name) 104 if err != nil { 105 return corrections, err 106 } 107 params[fmt.Sprintf("ADDRR%d", addrridx)] = newRecordString 108 addrridx++ 109 delrridx++ 110 } 111 msg := fmt.Sprintf("GENERATE_ZONEFILE: %s\n", dc.Name) + buf.String() 112 113 if changes { 114 corrections = append(corrections, &models.Correction{ 115 Msg: msg, 116 F: func() error { 117 return n.updateZoneBy(params, dc.Name) 118 }, 119 }) 120 } 121 return corrections, nil 122 } 123 124 func toRecord(r *HXRecord, origin string) *models.RecordConfig { 125 rc := &models.RecordConfig{ 126 Type: r.Type, 127 TTL: r.TTL, 128 Original: r, 129 } 130 fqdn := r.Fqdn[:len(r.Fqdn)-1] 131 rc.SetLabelFromFQDN(fqdn, origin) 132 133 switch rtype := r.Type; rtype { 134 case "TXT": 135 rc.SetTargetTXTs(decodeTxt(r.Answer)) 136 case "MX": 137 if err := rc.SetTargetMX(uint16(r.Priority), r.Answer); err != nil { 138 panic(fmt.Errorf("unparsable MX record received from hexonet api: %w", err)) 139 } 140 case "SRV": 141 if err := rc.SetTargetSRVPriorityString(uint16(r.Priority), r.Answer); err != nil { 142 panic(fmt.Errorf("unparsable SRV record received from hexonet api: %w", err)) 143 } 144 default: // "A", "AAAA", "ANAME", "CNAME", "NS" 145 if err := rc.PopulateFromString(rtype, r.Answer, r.Fqdn); err != nil { 146 panic(fmt.Errorf("unparsable record received from hexonet api: %w", err)) 147 } 148 } 149 return rc 150 } 151 152 func (n *HXClient) showCommand(cmd map[string]string) { 153 b, err := json.MarshalIndent(cmd, "", " ") 154 if err != nil { 155 fmt.Println("error:", err) 156 } 157 fmt.Print(string(b)) 158 } 159 160 func (n *HXClient) updateZoneBy(params map[string]string, domain string) error { 161 zone := domain + "." 162 cmd := map[string]string{ 163 "COMMAND": "UpdateDNSZone", 164 "DNSZONE": zone, 165 "INCSERIAL": "1", 166 } 167 for key, val := range params { 168 cmd[key] = val 169 } 170 // n.showCommand(cmd) 171 r := n.client.Request(cmd) 172 if !r.IsSuccess() { 173 return n.GetHXApiError("Error while updating zone", zone, r) 174 } 175 return nil 176 } 177 178 func (n *HXClient) getRecords(domain string) ([]*HXRecord, error) { 179 var records []*HXRecord 180 zone := domain + "." 181 cmd := map[string]string{ 182 "COMMAND": "QueryDNSZoneRRList", 183 "DNSZONE": zone, 184 "SHORT": "1", 185 "EXTENDED": "0", 186 } 187 r := n.client.Request(cmd) 188 if !r.IsSuccess() { 189 if r.GetCode() == 545 { 190 return nil, n.GetHXApiError("Use `dnscontrol create-domains` to create not-existing zone", domain, r) 191 } 192 return nil, n.GetHXApiError("Failed loading resource records for zone", domain, r) 193 } 194 rrColumn := r.GetColumn("RR") 195 if rrColumn == nil { 196 return nil, fmt.Errorf("Error getting RR column for domain: %s", domain) 197 } 198 rrs := rrColumn.GetData() 199 for _, rr := range rrs { 200 spl := strings.Split(rr, " ") 201 if spl[3] != "SOA" { 202 record := &HXRecord{ 203 Raw: rr, 204 DomainName: domain, 205 Host: spl[0], 206 Fqdn: domain + ".", 207 Type: spl[3], 208 } 209 ttl, _ := strconv.ParseUint(spl[1], 10, 32) 210 record.TTL = uint32(ttl) 211 if record.Host != "@" { 212 record.Fqdn = spl[0] + "." + record.Fqdn 213 } 214 if record.Type == "MX" || record.Type == "SRV" { 215 prio, _ := strconv.ParseUint(spl[4], 10, 32) 216 record.Priority = uint32(prio) 217 record.Answer = strings.Join(spl[5:], " ") 218 } else { 219 record.Answer = strings.Join(spl[4:], " ") 220 } 221 records = append(records, record) 222 } 223 } 224 return records, nil 225 } 226 227 func (n *HXClient) createRecordString(rc *models.RecordConfig, domain string) (string, error) { 228 record := &HXRecord{ 229 DomainName: domain, 230 Host: rc.GetLabel(), 231 Type: rc.Type, 232 Answer: rc.GetTargetField(), 233 TTL: rc.TTL, 234 Priority: uint32(rc.MxPreference), 235 } 236 switch rc.Type { // #rtype_variations 237 case "A", "AAAA", "ANAME", "CNAME", "MX", "NS", "PTR": 238 // nothing 239 case "TLSA": 240 record.Answer = fmt.Sprintf(`%v %v %v %s`, rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType, rc.Target) 241 case "CAA": 242 record.Answer = fmt.Sprintf(`%v %s "%s"`, rc.CaaFlag, rc.CaaTag, record.Answer) 243 case "TXT": 244 record.Answer = encodeTxt(rc.TxtStrings) 245 case "SRV": 246 if rc.GetTargetField() == "." { 247 return "", fmt.Errorf("SRV records with empty targets are not supported (as of 2020-02-27, the API returns 'Invalid attribute value syntax')") 248 } 249 record.Answer = fmt.Sprintf("%d %d %v", rc.SrvWeight, rc.SrvPort, record.Answer) 250 record.Priority = uint32(rc.SrvPriority) 251 default: 252 panic(fmt.Sprintf("createRecordString rtype %v unimplemented", rc.Type)) 253 // We panic so that we quickly find any switch statements 254 // that have not been updated for a new RR type. 255 } 256 257 str := record.Host + " " + fmt.Sprint(record.TTL) + " IN " + record.Type + " " 258 if record.Type == "MX" || record.Type == "SRV" { 259 str += fmt.Sprint(record.Priority) + " " 260 } 261 str += record.Answer 262 return str, nil 263 } 264 265 func (n *HXClient) deleteRecordString(record *HXRecord, domain string) string { 266 return record.Raw 267 } 268 269 // encodeTxt encodes TxtStrings for sending in the CREATE/MODIFY API: 270 func encodeTxt(txts []string) string { 271 ans := txts[0] 272 273 if len(txts) > 1 { 274 ans = "" 275 for _, t := range txts { 276 ans += `"` + strings.Replace(t, `"`, `\"`, -1) + `"` 277 } 278 } 279 return ans 280 } 281 282 // finds a string surrounded by quotes that might contain an escaped quote character. 283 var quotedStringRegexp = regexp.MustCompile(`"((?:[^"\\]|\\.)*)"`) 284 285 // decodeTxt decodes the TXT record as received from hexonet api and 286 // returns the list of strings. 287 func decodeTxt(s string) []string { 288 289 if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' { 290 txtStrings := []string{} 291 for _, t := range quotedStringRegexp.FindAllStringSubmatch(s, -1) { 292 txtString := strings.Replace(t[1], `\"`, `"`, -1) 293 txtStrings = append(txtStrings, txtString) 294 } 295 return txtStrings 296 } 297 return []string{s} 298 }