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  }