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