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

     1  package namecheap
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"sort"
     7  	"strings"
     8  	"time"
     9  
    10  	nc "github.com/billputer/go-namecheap"
    11  	"golang.org/x/net/publicsuffix"
    12  
    13  	"github.com/StackExchange/dnscontrol/v2/models"
    14  	"github.com/StackExchange/dnscontrol/v2/pkg/printer"
    15  	"github.com/StackExchange/dnscontrol/v2/providers"
    16  	"github.com/StackExchange/dnscontrol/v2/providers/diff"
    17  )
    18  
    19  // NamecheapDefaultNs lists the default nameservers for this provider.
    20  var NamecheapDefaultNs = []string{"dns1.registrar-servers.com", "dns2.registrar-servers.com"}
    21  
    22  // Namecheap is the handle for this provider.
    23  type Namecheap struct {
    24  	ApiKey  string
    25  	ApiUser string
    26  	client  *nc.Client
    27  }
    28  
    29  var features = providers.DocumentationNotes{
    30  	providers.CanUseAlias:            providers.Cannot(),
    31  	providers.CanUseCAA:              providers.Can(),
    32  	providers.CanUsePTR:              providers.Cannot(),
    33  	providers.CanUseSRV:              providers.Cannot("The namecheap web console allows you to make SRV records, but their api does not let you read or set them"),
    34  	providers.CanUseTLSA:             providers.Cannot(),
    35  	providers.CantUseNOPURGE:         providers.Cannot(),
    36  	providers.DocCreateDomains:       providers.Cannot("Requires domain registered through their service"),
    37  	providers.DocDualHost:            providers.Cannot("Doesn't allow control of apex NS records"),
    38  	providers.DocOfficiallySupported: providers.Cannot(),
    39  	providers.CanGetZones:            providers.Unimplemented(),
    40  }
    41  
    42  func init() {
    43  	providers.RegisterRegistrarType("NAMECHEAP", newReg)
    44  	providers.RegisterDomainServiceProviderType("NAMECHEAP", newDsp, features)
    45  	providers.RegisterCustomRecordType("URL", "NAMECHEAP", "")
    46  	providers.RegisterCustomRecordType("URL301", "NAMECHEAP", "")
    47  	providers.RegisterCustomRecordType("FRAME", "NAMECHEAP", "")
    48  }
    49  
    50  func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
    51  	return newProvider(conf, metadata)
    52  }
    53  
    54  func newReg(conf map[string]string) (providers.Registrar, error) {
    55  	return newProvider(conf, nil)
    56  }
    57  
    58  func newProvider(m map[string]string, metadata json.RawMessage) (*Namecheap, error) {
    59  	api := &Namecheap{}
    60  	api.ApiUser, api.ApiKey = m["apiuser"], m["apikey"]
    61  	if api.ApiKey == "" || api.ApiUser == "" {
    62  		return nil, fmt.Errorf("missing Namecheap apikey and apiuser")
    63  	}
    64  	api.client = nc.NewClient(api.ApiUser, api.ApiKey, api.ApiUser)
    65  	// if BaseURL is specified in creds, use that url
    66  	BaseURL, ok := m["BaseURL"]
    67  	if ok {
    68  		api.client.BaseURL = BaseURL
    69  	}
    70  	return api, nil
    71  }
    72  
    73  func splitDomain(domain string) (sld string, tld string) {
    74  	tld, _ = publicsuffix.PublicSuffix(domain)
    75  	d, _ := publicsuffix.EffectiveTLDPlusOne(domain)
    76  	sld = strings.Split(d, ".")[0]
    77  	return sld, tld
    78  }
    79  
    80  // namecheap has request limiting at unpublished limits
    81  // from support in SEP-2017:
    82  //    "The limits for the API calls will be 20/Min, 700/Hour and 8000/Day for one user.
    83  //     If you can limit the requests within these it should be fine."
    84  // this helper performs some api action, checks for rate limited response, and if so, enters a retry loop until it resolves
    85  // if you are consistently hitting this, you may have success asking their support to increase your account's limits.
    86  func doWithRetry(f func() error) {
    87  	// sleep 5 seconds at a time, up to 23 times (1 minute, 15 seconds)
    88  	const maxRetries = 23
    89  	const sleepTime = 5 * time.Second
    90  	var currentRetry int
    91  	for {
    92  		err := f()
    93  		if err == nil {
    94  			return
    95  		}
    96  		if strings.Contains(err.Error(), "Error 500000: Too many requests") {
    97  			currentRetry++
    98  			if currentRetry >= maxRetries {
    99  				return
   100  			}
   101  			printer.Printf("Namecheap rate limit exceeded. Waiting %s to retry.\n", sleepTime)
   102  			time.Sleep(sleepTime)
   103  		} else {
   104  			return
   105  		}
   106  	}
   107  }
   108  
   109  // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
   110  func (client *Namecheap) GetZoneRecords(domain string) (models.Records, error) {
   111  	return nil, fmt.Errorf("not implemented")
   112  	// This enables the get-zones subcommand.
   113  	// Implement this by extracting the code from GetDomainCorrections into
   114  	// a single function.  For most providers this should be relatively easy.
   115  }
   116  
   117  // GetDomainCorrections returns the corrections for the domain.
   118  func (n *Namecheap) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
   119  	dc.Punycode()
   120  	sld, tld := splitDomain(dc.Name)
   121  	var records *nc.DomainDNSGetHostsResult
   122  	var err error
   123  	doWithRetry(func() error {
   124  		records, err = n.client.DomainsDNSGetHosts(sld, tld)
   125  		return err
   126  	})
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	var actual []*models.RecordConfig
   132  
   133  	// namecheap does not allow setting @ NS with basic DNS
   134  	dc.Filter(func(r *models.RecordConfig) bool {
   135  		if r.Type == "NS" && r.GetLabel() == "@" {
   136  			if !strings.HasSuffix(r.GetTargetField(), "registrar-servers.com.") {
   137  				fmt.Println("\n", r.GetTargetField(), "Namecheap does not support changing apex NS records. Skipping.")
   138  			}
   139  			return false
   140  		}
   141  		return true
   142  	})
   143  
   144  	// namecheap has this really annoying feature where they add some parking records if you have no records.
   145  	// This causes a few problems for our purposes, specifically the integration tests.
   146  	// lets detect that one case and pretend it is a no-op.
   147  	if len(dc.Records) == 0 && len(records.Hosts) == 2 {
   148  		if records.Hosts[0].Type == "CNAME" &&
   149  			strings.Contains(records.Hosts[0].Address, "parkingpage") &&
   150  			records.Hosts[1].Type == "URL" {
   151  			return nil, nil
   152  		}
   153  	}
   154  
   155  	for _, r := range records.Hosts {
   156  		if r.Type == "SOA" {
   157  			continue
   158  		}
   159  		rec := &models.RecordConfig{
   160  			Type:         r.Type,
   161  			TTL:          uint32(r.TTL),
   162  			MxPreference: uint16(r.MXPref),
   163  			Original:     r,
   164  		}
   165  		rec.SetLabel(r.Name, dc.Name)
   166  		switch rtype := r.Type; rtype { // #rtype_variations
   167  		case "TXT":
   168  			rec.SetTargetTXT(r.Address)
   169  		case "CAA":
   170  			rec.SetTargetCAAString(r.Address)
   171  		default:
   172  			rec.SetTarget(r.Address)
   173  		}
   174  		actual = append(actual, rec)
   175  	}
   176  
   177  	// Normalize
   178  	models.PostProcessRecords(actual)
   179  
   180  	differ := diff.New(dc)
   181  	_, create, delete, modify := differ.IncrementalDiff(actual)
   182  
   183  	// // because namecheap doesn't have selective create, delete, modify,
   184  	// // we bundle them all up to send at once.  We *do* want to see the
   185  	// // changes though
   186  
   187  	var desc []string
   188  	for _, i := range create {
   189  		desc = append(desc, "\n"+i.String())
   190  	}
   191  	for _, i := range delete {
   192  		desc = append(desc, "\n"+i.String())
   193  	}
   194  	for _, i := range modify {
   195  		desc = append(desc, "\n"+i.String())
   196  	}
   197  
   198  	msg := fmt.Sprintf("GENERATE_ZONE: %s (%d records)%s", dc.Name, len(dc.Records), desc)
   199  	corrections := []*models.Correction{}
   200  
   201  	// only create corrections if there are changes
   202  	if len(desc) > 0 {
   203  		corrections = append(corrections,
   204  			&models.Correction{
   205  				Msg: msg,
   206  				F: func() error {
   207  					return n.generateRecords(dc)
   208  				},
   209  			})
   210  	}
   211  
   212  	return corrections, nil
   213  }
   214  
   215  func (n *Namecheap) generateRecords(dc *models.DomainConfig) error {
   216  
   217  	var recs []nc.DomainDNSHost
   218  
   219  	id := 1
   220  	for _, r := range dc.Records {
   221  		var value string
   222  		switch rtype := r.Type; rtype { // #rtype_variations
   223  		case "CAA":
   224  			value = r.GetTargetCombined()
   225  		default:
   226  			value = r.GetTargetField()
   227  		}
   228  
   229  		rec := nc.DomainDNSHost{
   230  			ID:      id,
   231  			Name:    r.GetLabel(),
   232  			Type:    r.Type,
   233  			Address: value,
   234  			MXPref:  int(r.MxPreference),
   235  			TTL:     int(r.TTL),
   236  		}
   237  		recs = append(recs, rec)
   238  		id++
   239  	}
   240  	sld, tld := splitDomain(dc.Name)
   241  	var err error
   242  	doWithRetry(func() error {
   243  		_, err = n.client.DomainDNSSetHosts(sld, tld, recs)
   244  		return err
   245  	})
   246  	return err
   247  }
   248  
   249  // GetNameservers returns the nameservers for a domain.
   250  func (n *Namecheap) GetNameservers(domainName string) ([]*models.Nameserver, error) {
   251  	// return default namecheap nameservers
   252  	ns := NamecheapDefaultNs
   253  
   254  	return models.StringsToNameservers(ns), nil
   255  }
   256  
   257  // GetRegistrarCorrections returns corrections to update nameservers.
   258  func (n *Namecheap) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
   259  	var info *nc.DomainInfo
   260  	var err error
   261  	doWithRetry(func() error {
   262  		info, err = n.client.DomainGetInfo(dc.Name)
   263  		return err
   264  	})
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	sort.Strings(info.DNSDetails.Nameservers)
   269  	found := strings.Join(info.DNSDetails.Nameservers, ",")
   270  	desiredNs := []string{}
   271  	for _, d := range dc.Nameservers {
   272  		desiredNs = append(desiredNs, d.Name)
   273  	}
   274  	sort.Strings(desiredNs)
   275  	desired := strings.Join(desiredNs, ",")
   276  	if found != desired {
   277  		parts := strings.SplitN(dc.Name, ".", 2)
   278  		sld, tld := parts[0], parts[1]
   279  		return []*models.Correction{
   280  			{
   281  				Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", found, desired),
   282  				F: func() (err error) {
   283  					doWithRetry(func() error {
   284  						_, err = n.client.DomainDNSSetCustom(sld, tld, desired)
   285  						return err
   286  					})
   287  					return
   288  				}},
   289  		}, nil
   290  	}
   291  	return nil, nil
   292  }