github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/providers/namecheap/namecheap.go (about)

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