github.com/stackexchange/dnscontrol@v0.2.8/providers/namecheap/namecheapProvider.go (about)

     1  package namecheap
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"sort"
     7  	"strings"
     8  	"time"
     9  
    10  	"golang.org/x/net/publicsuffix"
    11  
    12  	"github.com/StackExchange/dnscontrol/models"
    13  	"github.com/StackExchange/dnscontrol/pkg/printer"
    14  	"github.com/StackExchange/dnscontrol/providers"
    15  	"github.com/StackExchange/dnscontrol/providers/diff"
    16  	nc "github.com/billputer/go-namecheap"
    17  	"github.com/pkg/errors"
    18  )
    19  
    20  // NamecheapDefaultNs lists the default nameservers for this provider.
    21  var NamecheapDefaultNs = []string{"dns1.registrar-servers.com", "dns2.registrar-servers.com"}
    22  
    23  // Namecheap is the handle for this provider.
    24  type Namecheap struct {
    25  	ApiKey  string
    26  	ApiUser string
    27  	client  *nc.Client
    28  }
    29  
    30  var features = providers.DocumentationNotes{
    31  	providers.CanUseAlias:            providers.Cannot(),
    32  	providers.CanUseCAA:              providers.Cannot(),
    33  	providers.CanUsePTR:              providers.Cannot(),
    34  	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"),
    35  	providers.CanUseTLSA:             providers.Cannot(),
    36  	providers.CantUseNOPURGE:         providers.Cannot(),
    37  	providers.DocCreateDomains:       providers.Cannot("Requires domain registered through their service"),
    38  	providers.DocDualHost:            providers.Cannot("Doesn't allow control of apex NS records"),
    39  	providers.DocOfficiallySupported: providers.Cannot(),
    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, errors.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  // GetDomainCorrections returns the corrections for the domain.
   110  func (n *Namecheap) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
   111  	dc.Punycode()
   112  	sld, tld := splitDomain(dc.Name)
   113  	var records *nc.DomainDNSGetHostsResult
   114  	var err error
   115  	doWithRetry(func() error {
   116  		records, err = n.client.DomainsDNSGetHosts(sld, tld)
   117  		return err
   118  	})
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	var actual []*models.RecordConfig
   124  
   125  	// namecheap does not allow setting @ NS with basic DNS
   126  	dc.Filter(func(r *models.RecordConfig) bool {
   127  		if r.Type == "NS" && r.GetLabel() == "@" {
   128  			if !strings.HasSuffix(r.GetTargetField(), "registrar-servers.com.") {
   129  				fmt.Println("\n", r.GetTargetField(), "Namecheap does not support changing apex NS records. Skipping.")
   130  			}
   131  			return false
   132  		}
   133  		return true
   134  	})
   135  
   136  	// namecheap has this really annoying feature where they add some parking records if you have no records.
   137  	// This causes a few problems for our purposes, specifically the integration tests.
   138  	// lets detect that one case and pretend it is a no-op.
   139  	if len(dc.Records) == 0 && len(records.Hosts) == 2 {
   140  		if records.Hosts[0].Type == "CNAME" &&
   141  			strings.Contains(records.Hosts[0].Address, "parkingpage") &&
   142  			records.Hosts[1].Type == "URL" {
   143  			return nil, nil
   144  		}
   145  	}
   146  
   147  	for _, r := range records.Hosts {
   148  		if r.Type == "SOA" {
   149  			continue
   150  		}
   151  		rec := &models.RecordConfig{
   152  			Type:         r.Type,
   153  			TTL:          uint32(r.TTL),
   154  			MxPreference: uint16(r.MXPref),
   155  			Original:     r,
   156  		}
   157  		rec.SetLabel(r.Name, dc.Name)
   158  		rec.SetTarget(r.Address)
   159  		actual = append(actual, rec)
   160  	}
   161  
   162  	// Normalize
   163  	models.PostProcessRecords(actual)
   164  
   165  	differ := diff.New(dc)
   166  	_, create, delete, modify := differ.IncrementalDiff(actual)
   167  
   168  	// // because namecheap doesn't have selective create, delete, modify,
   169  	// // we bundle them all up to send at once.  We *do* want to see the
   170  	// // changes though
   171  
   172  	var desc []string
   173  	for _, i := range create {
   174  		desc = append(desc, "\n"+i.String())
   175  	}
   176  	for _, i := range delete {
   177  		desc = append(desc, "\n"+i.String())
   178  	}
   179  	for _, i := range modify {
   180  		desc = append(desc, "\n"+i.String())
   181  	}
   182  
   183  	msg := fmt.Sprintf("GENERATE_ZONE: %s (%d records)%s", dc.Name, len(dc.Records), desc)
   184  	corrections := []*models.Correction{}
   185  
   186  	// only create corrections if there are changes
   187  	if len(desc) > 0 {
   188  		corrections = append(corrections,
   189  			&models.Correction{
   190  				Msg: msg,
   191  				F: func() error {
   192  					return n.generateRecords(dc)
   193  				},
   194  			})
   195  	}
   196  
   197  	return corrections, nil
   198  }
   199  
   200  func (n *Namecheap) generateRecords(dc *models.DomainConfig) error {
   201  
   202  	var recs []nc.DomainDNSHost
   203  
   204  	id := 1
   205  	for _, r := range dc.Records {
   206  		rec := nc.DomainDNSHost{
   207  			ID:      id,
   208  			Name:    r.GetLabel(),
   209  			Type:    r.Type,
   210  			Address: r.GetTargetField(),
   211  			MXPref:  int(r.MxPreference),
   212  			TTL:     int(r.TTL),
   213  		}
   214  		recs = append(recs, rec)
   215  		id++
   216  	}
   217  	sld, tld := splitDomain(dc.Name)
   218  	var err error
   219  	doWithRetry(func() error {
   220  		_, err = n.client.DomainDNSSetHosts(sld, tld, recs)
   221  		return err
   222  	})
   223  	return err
   224  }
   225  
   226  // GetNameservers returns the nameservers for a domain.
   227  func (n *Namecheap) GetNameservers(domainName string) ([]*models.Nameserver, error) {
   228  	// return default namecheap nameservers
   229  	ns := NamecheapDefaultNs
   230  
   231  	return models.StringsToNameservers(ns), nil
   232  }
   233  
   234  // GetRegistrarCorrections returns corrections to update nameservers.
   235  func (n *Namecheap) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
   236  	var info *nc.DomainInfo
   237  	var err error
   238  	doWithRetry(func() error {
   239  		info, err = n.client.DomainGetInfo(dc.Name)
   240  		return err
   241  	})
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  	sort.Strings(info.DNSDetails.Nameservers)
   246  	found := strings.Join(info.DNSDetails.Nameservers, ",")
   247  	desiredNs := []string{}
   248  	for _, d := range dc.Nameservers {
   249  		desiredNs = append(desiredNs, d.Name)
   250  	}
   251  	sort.Strings(desiredNs)
   252  	desired := strings.Join(desiredNs, ",")
   253  	if found != desired {
   254  		parts := strings.SplitN(dc.Name, ".", 2)
   255  		sld, tld := parts[0], parts[1]
   256  		return []*models.Correction{
   257  			{
   258  				Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", found, desired),
   259  				F: func() (err error) {
   260  					doWithRetry(func() error {
   261  						_, err = n.client.DomainDNSSetCustom(sld, tld, desired)
   262  						return err
   263  					})
   264  					return
   265  				}},
   266  		}, nil
   267  	}
   268  	return nil, nil
   269  }