package namecheap import ( "encoding/json" "fmt" "sort" "strings" "time" "golang.org/x/net/publicsuffix" "github.com/StackExchange/dnscontrol/models" "github.com/StackExchange/dnscontrol/providers" "github.com/StackExchange/dnscontrol/providers/diff" nc "github.com/billputer/go-namecheap" "github.com/miekg/dns/dnsutil" ) var NamecheapDefaultNs = []string{"dns1.registrar-servers.com", "dns2.registrar-servers.com"} type Namecheap struct { ApiKey string ApiUser string client *nc.Client } var docNotes = providers.DocumentationNotes{ providers.DocCreateDomains: providers.Cannot("Requires domain registered through their service"), providers.DocOfficiallySupported: providers.Cannot(), providers.DocDualHost: providers.Cannot("Doesn't allow control of apex NS records"), providers.CanUseAlias: providers.Cannot(), providers.CanUseCAA: providers.Cannot(), providers.CanUseSRV: providers.Unimplemented("namecheap supports srv records, we just need someone to implement it and make sure the tests pass."), providers.CanUsePTR: providers.Cannot(), providers.CanUseTLSA: providers.Cannot(), } func init() { providers.RegisterRegistrarType("NAMECHEAP", newReg, docNotes) providers.RegisterDomainServiceProviderType("NAMECHEAP", newDsp, providers.CantUseNOPURGE) providers.RegisterCustomRecordType("URL", "NAMECHEAP", "") providers.RegisterCustomRecordType("URL301", "NAMECHEAP", "") providers.RegisterCustomRecordType("FRAME", "NAMECHEAP", "") } func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { return newProvider(conf, metadata) } func newReg(conf map[string]string) (providers.Registrar, error) { return newProvider(conf, nil) } func newProvider(m map[string]string, metadata json.RawMessage) (*Namecheap, error) { api := &Namecheap{} api.ApiUser, api.ApiKey = m["apiuser"], m["apikey"] if api.ApiKey == "" || api.ApiUser == "" { return nil, fmt.Errorf("Namecheap apikey and apiuser must be provided.") } api.client = nc.NewClient(api.ApiUser, api.ApiKey, api.ApiUser) // if BaseURL is specified in creds, use that url BaseURL, ok := m["BaseURL"] if ok { api.client.BaseURL = BaseURL } return api, nil } func splitDomain(domain string) (sld string, tld string) { tld, _ = publicsuffix.PublicSuffix(domain) d, _ := publicsuffix.EffectiveTLDPlusOne(domain) sld = strings.Split(d, ".")[0] return sld, tld } // namecheap has request limiting at unpublished limits // this channel acts as a global rate limiter // read from it before every request // from support in SEP-2017: // "The limits for the API calls will be 20/Min, 700/Hour and 8000/Day for one user. // If you can limit the requests within these it should be fine." var throttle = make(chan bool, 20) func init() { go func() { for { // add (up to) 20 requests every minute for i := 0; i < 20; i++ { select { case throttle <- true: default: } } time.Sleep(time.Minute) } }() } func doThrottle() { select { case <-throttle: default: fmt.Println("\nNamecheap request limit reached. Waiting for more requests to be available") <-throttle } } func (n *Namecheap) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { dc.Punycode() sld, tld := splitDomain(dc.Name) doThrottle() records, err := n.client.DomainsDNSGetHosts(sld, tld) if err != nil { return nil, err } var actual []*models.RecordConfig // namecheap does not allow setting @ NS with basic DNS dc.Filter(func(r *models.RecordConfig) bool { if r.Type == "NS" && r.Name == "@" { if !strings.HasSuffix(r.Target, "registrar-servers.com.") { fmt.Println("\n", r.Target, "Namecheap does not support changing apex NS records. Skipping.") } return false } return true }) // namecheap has this really annoying feature where they add some parking records if you have no records. // This causes a few problems for our purposes, specifically the integration tests. // lets detect that one case and pretend it is a no-op. if len(dc.Records) == 0 && len(records.Hosts) == 2 { if records.Hosts[0].Type == "CNAME" && strings.Contains(records.Hosts[0].Address, "parkingpage") && records.Hosts[1].Type == "URL" { return nil, nil } } for _, r := range records.Hosts { if r.Type == "SOA" { continue } rec := &models.RecordConfig{ NameFQDN: dnsutil.AddOrigin(r.Name, dc.Name), Type: r.Type, Target: r.Address, TTL: uint32(r.TTL), MxPreference: uint16(r.MXPref), Original: r, } actual = append(actual, rec) } differ := diff.New(dc) _, create, delete, modify := differ.IncrementalDiff(actual) // // because namecheap doesn't have selective create, delete, modify, // // we bundle them all up to send at once. We *do* want to see the // // changes though var desc []string for _, i := range create { desc = append(desc, "\n"+i.String()) } for _, i := range delete { desc = append(desc, "\n"+i.String()) } for _, i := range modify { desc = append(desc, "\n"+i.String()) } msg := fmt.Sprintf("GENERATE_ZONE: %s (%d records)%s", dc.Name, len(dc.Records), desc) corrections := []*models.Correction{} // only create corrections if there are changes if len(desc) > 0 { corrections = append(corrections, &models.Correction{ Msg: msg, F: func() error { return n.generateRecords(dc) }, }) } return corrections, nil } func (n *Namecheap) generateRecords(dc *models.DomainConfig) error { var recs []nc.DomainDNSHost id := 1 for _, r := range dc.Records { name := dnsutil.TrimDomainName(r.NameFQDN, dc.Name) rec := nc.DomainDNSHost{ ID: id, Name: name, Type: r.Type, Address: r.Target, MXPref: int(r.MxPreference), TTL: int(r.TTL), } recs = append(recs, rec) id++ } sld, tld := splitDomain(dc.Name) <-throttle _, err := n.client.DomainDNSSetHosts(sld, tld, recs) return err } func (n *Namecheap) GetNameservers(domainName string) ([]*models.Nameserver, error) { // return default namecheap nameservers ns := NamecheapDefaultNs return models.StringsToNameservers(ns), nil } func (n *Namecheap) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { doThrottle() info, err := n.client.DomainGetInfo(dc.Name) if err != nil { return nil, err } sort.Strings(info.DNSDetails.Nameservers) found := strings.Join(info.DNSDetails.Nameservers, ",") desiredNs := []string{} for _, d := range dc.Nameservers { desiredNs = append(desiredNs, d.Name) } sort.Strings(desiredNs) desired := strings.Join(desiredNs, ",") if found != desired { parts := strings.SplitN(dc.Name, ".", 2) sld, tld := parts[0], parts[1] return []*models.Correction{ { Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", found, desired), F: func() error { doThrottle() _, err := n.client.DomainDNSSetCustom(sld, tld, desired) if err != nil { return err } return nil }}, }, nil } return nil, nil }