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  }