github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/cloudns/cloudnsProvider.go (about)

     1  package cloudns
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strconv"
     7  
     8  	"github.com/miekg/dns/dnsutil"
     9  
    10  	"github.com/StackExchange/dnscontrol/v2/models"
    11  	"github.com/StackExchange/dnscontrol/v2/providers"
    12  	"github.com/StackExchange/dnscontrol/v2/providers/diff"
    13  )
    14  
    15  /*
    16  
    17  CloDNS API DNS provider:
    18  
    19  Info required in `creds.json`:
    20     - auth-id
    21     - auth-password
    22  
    23  */
    24  
    25  // NewCloudns creates the provider.
    26  func NewCloudns(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
    27  	c := &api{}
    28  
    29  	c.creds.id, c.creds.password = m["auth-id"], m["auth-password"]
    30  	if c.creds.id == "" || c.creds.password == "" {
    31  		return nil, fmt.Errorf("missing ClouDNS auth-id and auth-password")
    32  	}
    33  
    34  	// Get a domain to validate authentication
    35  	if err := c.fetchDomainList(); err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	return c, nil
    40  }
    41  
    42  var features = providers.DocumentationNotes{
    43  	providers.DocDualHost:            providers.Unimplemented(),
    44  	providers.DocOfficiallySupported: providers.Cannot(),
    45  	providers.DocCreateDomains:       providers.Can(),
    46  	providers.CanUseAlias:            providers.Can(),
    47  	providers.CanUseSRV:              providers.Can(),
    48  	providers.CanUseSSHFP:            providers.Can(),
    49  	providers.CanUseCAA:              providers.Can(),
    50  	providers.CanUseTLSA:             providers.Can(),
    51  	providers.CanUsePTR:              providers.Unimplemented(),
    52  	providers.CanGetZones:            providers.Unimplemented(),
    53  }
    54  
    55  func init() {
    56  	providers.RegisterDomainServiceProviderType("CLOUDNS", NewCloudns, features)
    57  }
    58  
    59  // GetNameservers returns the nameservers for a domain.
    60  func (c *api) GetNameservers(domain string) ([]*models.Nameserver, error) {
    61  	if len(c.nameserversNames) == 0 {
    62  		c.fetchAvailableNameservers()
    63  	}
    64  	return models.StringsToNameservers(c.nameserversNames), nil
    65  }
    66  
    67  // GetDomainCorrections returns the corrections for a domain.
    68  func (c *api) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
    69  	dc, err := dc.Copy()
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	dc.Punycode()
    75  
    76  	if c.domainIndex == nil {
    77  		if err := c.fetchDomainList(); err != nil {
    78  			return nil, err
    79  		}
    80  	}
    81  	domainID, ok := c.domainIndex[dc.Name]
    82  	if !ok {
    83  		return nil, fmt.Errorf("'%s' not a zone in ClouDNS account", dc.Name)
    84  	}
    85  
    86  	records, err := c.getRecords(domainID)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	existingRecords := make([]*models.RecordConfig, len(records), len(records)+len(c.nameserversNames))
    92  	for i := range records {
    93  		existingRecords[i] = toRc(dc, &records[i])
    94  	}
    95  	// Normalize
    96  	models.PostProcessRecords(existingRecords)
    97  
    98  	// ClouDNS doesn't allow selecting an arbitrary TTL, only a set of predefined values https://asia.cloudns.net/wiki/article/188/
    99  	// We need to make sure we don't change it every time if it is as close as it's going to get
   100  	for _, record := range dc.Records {
   101  		record.TTL = fixTTL(record.TTL)
   102  	}
   103  
   104  	differ := diff.New(dc)
   105  	_, create, del, modify := differ.IncrementalDiff(existingRecords)
   106  
   107  	var corrections []*models.Correction
   108  
   109  	// Deletes first so changing type works etc.
   110  	for _, m := range del {
   111  		id := m.Existing.Original.(*domainRecord).ID
   112  		corr := &models.Correction{
   113  			Msg: fmt.Sprintf("%s, ClouDNS ID: %s", m.String(), id),
   114  			F: func() error {
   115  				return c.deleteRecord(domainID, id)
   116  			},
   117  		}
   118  		corrections = append(corrections, corr)
   119  	}
   120  
   121  	for _, m := range create {
   122  		req, err := toReq(m.Desired)
   123  		if err != nil {
   124  			return nil, err
   125  		}
   126  
   127  		corr := &models.Correction{
   128  			Msg: m.String(),
   129  			F: func() error {
   130  				return c.createRecord(domainID, req)
   131  			},
   132  		}
   133  		corrections = append(corrections, corr)
   134  	}
   135  	for _, m := range modify {
   136  		id := m.Existing.Original.(*domainRecord).ID
   137  		req, err := toReq(m.Desired)
   138  		if err != nil {
   139  			return nil, err
   140  		}
   141  
   142  		corr := &models.Correction{
   143  			Msg: fmt.Sprintf("%s, ClouDNS ID: %s: ", m.String(), id),
   144  			F: func() error {
   145  				return c.modifyRecord(domainID, id, req)
   146  			},
   147  		}
   148  		corrections = append(corrections, corr)
   149  	}
   150  
   151  	return corrections, nil
   152  }
   153  
   154  // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
   155  func (client *api) GetZoneRecords(domain string) (models.Records, error) {
   156  	return nil, fmt.Errorf("not implemented")
   157  	// This enables the get-zones subcommand.
   158  	// Implement this by extracting the code from GetDomainCorrections into
   159  	// a single function.  For most providers this should be relatively easy.
   160  }
   161  
   162  // EnsureDomainExists returns an error if domain doesn't exist.
   163  func (c *api) EnsureDomainExists(domain string) error {
   164  	if err := c.fetchDomainList(); err != nil {
   165  		return err
   166  	}
   167  	// domain already exists
   168  	if _, ok := c.domainIndex[domain]; ok {
   169  		return nil
   170  	}
   171  	return c.createDomain(domain)
   172  }
   173  
   174  func toRc(dc *models.DomainConfig, r *domainRecord) *models.RecordConfig {
   175  
   176  	ttl, _ := strconv.ParseUint(r.TTL, 10, 32)
   177  	priority, _ := strconv.ParseUint(r.Priority, 10, 32)
   178  	weight, _ := strconv.ParseUint(r.Weight, 10, 32)
   179  	port, _ := strconv.ParseUint(r.Port, 10, 32)
   180  
   181  	rc := &models.RecordConfig{
   182  		Type:         r.Type,
   183  		TTL:          uint32(ttl),
   184  		MxPreference: uint16(priority),
   185  		SrvPriority:  uint16(priority),
   186  		SrvWeight:    uint16(weight),
   187  		SrvPort:      uint16(port),
   188  		Original:     r,
   189  	}
   190  	rc.SetLabel(r.Host, dc.Name)
   191  
   192  	switch rtype := r.Type; rtype { // #rtype_variations
   193  	case "TXT":
   194  		rc.SetTargetTXT(r.Target)
   195  	case "CNAME", "MX", "NS", "SRV", "ALIAS":
   196  		rc.SetTarget(dnsutil.AddOrigin(r.Target+".", dc.Name))
   197  	case "CAA":
   198  		caaFlag, _ := strconv.ParseUint(r.CaaFlag, 10, 32)
   199  		rc.CaaFlag = uint8(caaFlag)
   200  		rc.CaaTag = r.CaaTag
   201  		rc.SetTarget(r.CaaValue)
   202  	case "TLSA":
   203  		tlsaUsage, _ := strconv.ParseUint(r.TlsaUsage, 10, 32)
   204  		rc.TlsaUsage = uint8(tlsaUsage)
   205  		tlsaSelector, _ := strconv.ParseUint(r.TlsaSelector, 10, 32)
   206  		rc.TlsaSelector = uint8(tlsaSelector)
   207  		tlsaMatchingType, _ := strconv.ParseUint(r.TlsaMatchingType, 10, 32)
   208  		rc.TlsaMatchingType = uint8(tlsaMatchingType)
   209  		rc.SetTarget(r.Target)
   210  	case "SSHFP":
   211  		sshfpAlgorithm, _ := strconv.ParseUint(r.SshfpAlgorithm, 10, 32)
   212  		rc.SshfpAlgorithm = uint8(sshfpAlgorithm)
   213  		sshfpFingerprint, _ := strconv.ParseUint(r.SshfpFingerprint, 10, 32)
   214  		rc.SshfpFingerprint = uint8(sshfpFingerprint)
   215  		rc.SetTarget(r.Target)
   216  	default:
   217  		rc.SetTarget(r.Target)
   218  	}
   219  
   220  	return rc
   221  }
   222  
   223  func toReq(rc *models.RecordConfig) (requestParams, error) {
   224  	req := requestParams{
   225  		"record-type": rc.Type,
   226  		"host":        rc.GetLabel(),
   227  		"record":      rc.GetTargetField(),
   228  		"ttl":         strconv.Itoa(int(rc.TTL)),
   229  	}
   230  
   231  	// ClouDNS doesn't use "@", it uses an empty name
   232  	if req["host"] == "@" {
   233  		req["host"] = ""
   234  	}
   235  
   236  	switch rc.Type { // #rtype_variations
   237  	case "A", "AAAA", "NS", "PTR", "TXT", "SOA", "ALIAS", "CNAME":
   238  		// Nothing special.
   239  	case "MX":
   240  		req["priority"] = strconv.Itoa(int(rc.MxPreference))
   241  	case "SRV":
   242  		req["priority"] = strconv.Itoa(int(rc.SrvPriority))
   243  		req["weight"] = strconv.Itoa(int(rc.SrvWeight))
   244  		req["port"] = strconv.Itoa(int(rc.SrvPort))
   245  	case "CAA":
   246  		req["caa_flag"] = strconv.Itoa(int(rc.CaaFlag))
   247  		req["caa_type"] = rc.CaaTag
   248  		req["caa_value"] = rc.Target
   249  	case "TLSA":
   250  		req["tlsa_usage"] = strconv.Itoa(int(rc.TlsaUsage))
   251  		req["tlsa_selector"] = strconv.Itoa(int(rc.TlsaSelector))
   252  		req["tlsa_matching_type"] = strconv.Itoa(int(rc.TlsaMatchingType))
   253  	case "SSHFP":
   254  		req["algorithm"] = strconv.Itoa(int(rc.SshfpAlgorithm))
   255  		req["fptype"] = strconv.Itoa(int(rc.SshfpFingerprint))
   256  	default:
   257  		msg := fmt.Sprintf("ClouDNS.toReq rtype %v unimplemented", rc.Type)
   258  		panic(msg)
   259  		// We panic so that we quickly find any switch statements
   260  	}
   261  
   262  	return req, nil
   263  }