2016-08-23 08:31:50 +08:00
|
|
|
package namecheap
|
|
|
|
|
|
|
|
import (
|
2017-10-03 23:53:56 +08:00
|
|
|
"encoding/json"
|
2016-08-23 08:31:50 +08:00
|
|
|
"fmt"
|
2017-04-11 09:26:19 +08:00
|
|
|
"sort"
|
2016-08-23 08:31:50 +08:00
|
|
|
"strings"
|
2017-10-16 23:21:33 +08:00
|
|
|
"time"
|
2016-08-23 08:31:50 +08:00
|
|
|
|
2017-10-03 23:53:56 +08:00
|
|
|
"golang.org/x/net/publicsuffix"
|
|
|
|
|
2016-08-23 08:31:50 +08:00
|
|
|
"github.com/StackExchange/dnscontrol/models"
|
|
|
|
"github.com/StackExchange/dnscontrol/providers"
|
2017-10-03 23:53:56 +08:00
|
|
|
"github.com/StackExchange/dnscontrol/providers/diff"
|
2017-01-12 22:31:32 +08:00
|
|
|
nc "github.com/billputer/go-namecheap"
|
2017-10-03 23:53:56 +08:00
|
|
|
"github.com/miekg/dns/dnsutil"
|
2016-08-23 08:31:50 +08:00
|
|
|
)
|
|
|
|
|
2017-10-03 23:53:56 +08:00
|
|
|
var NamecheapDefaultNs = []string{"dns1.registrar-servers.com", "dns2.registrar-servers.com"}
|
|
|
|
|
2016-08-23 08:31:50 +08:00
|
|
|
type Namecheap struct {
|
|
|
|
ApiKey string
|
|
|
|
ApiUser string
|
|
|
|
client *nc.Client
|
|
|
|
}
|
|
|
|
|
2017-09-15 04:13:17 +08:00
|
|
|
var docNotes = providers.DocumentationNotes{
|
|
|
|
providers.DocCreateDomains: providers.Cannot("Requires domain registered through their service"),
|
|
|
|
providers.DocOfficiallySupported: providers.Cannot(),
|
2017-10-03 23:53:56 +08:00
|
|
|
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(),
|
2017-09-15 04:13:17 +08:00
|
|
|
}
|
|
|
|
|
2016-08-23 08:31:50 +08:00
|
|
|
func init() {
|
2017-09-15 04:13:17 +08:00
|
|
|
providers.RegisterRegistrarType("NAMECHEAP", newReg, docNotes)
|
2017-10-03 23:53:56 +08:00
|
|
|
providers.RegisterDomainServiceProviderType("NAMECHEAP", newDsp, providers.CantUseNOPURGE)
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2016-08-23 08:31:50 +08:00
|
|
|
}
|
|
|
|
|
2017-10-03 23:53:56 +08:00
|
|
|
func newProvider(m map[string]string, metadata json.RawMessage) (*Namecheap, error) {
|
2016-08-23 08:31:50 +08:00
|
|
|
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)
|
2017-04-25 23:24:55 +08:00
|
|
|
// if BaseURL is specified in creds, use that url
|
|
|
|
BaseURL, ok := m["BaseURL"]
|
|
|
|
if ok {
|
|
|
|
api.client.BaseURL = BaseURL
|
|
|
|
}
|
2016-08-23 08:31:50 +08:00
|
|
|
return api, nil
|
|
|
|
}
|
|
|
|
|
2017-10-03 23:53:56 +08:00
|
|
|
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:
|
|
|
|
}
|
|
|
|
}
|
2017-10-16 23:21:33 +08:00
|
|
|
time.Sleep(time.Minute)
|
2017-10-03 23:53:56 +08:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (n *Namecheap) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
|
|
dc.Punycode()
|
|
|
|
sld, tld := splitDomain(dc.Name)
|
|
|
|
<-throttle
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2016-08-23 08:31:50 +08:00
|
|
|
func (n *Namecheap) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
2017-10-03 23:53:56 +08:00
|
|
|
<-throttle
|
2016-08-23 08:31:50 +08:00
|
|
|
info, err := n.client.DomainGetInfo(dc.Name)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-04-11 09:26:19 +08:00
|
|
|
sort.Strings(info.DNSDetails.Nameservers)
|
2016-08-23 08:31:50 +08:00
|
|
|
found := strings.Join(info.DNSDetails.Nameservers, ",")
|
2017-04-11 09:26:19 +08:00
|
|
|
desiredNs := []string{}
|
2016-08-23 08:31:50 +08:00
|
|
|
for _, d := range dc.Nameservers {
|
2017-04-11 09:26:19 +08:00
|
|
|
desiredNs = append(desiredNs, d.Name)
|
2016-08-23 08:31:50 +08:00
|
|
|
}
|
2017-04-11 09:26:19 +08:00
|
|
|
sort.Strings(desiredNs)
|
|
|
|
desired := strings.Join(desiredNs, ",")
|
2016-08-23 08:31:50 +08:00
|
|
|
if found != desired {
|
|
|
|
parts := strings.SplitN(dc.Name, ".", 2)
|
|
|
|
sld, tld := parts[0], parts[1]
|
|
|
|
return []*models.Correction{
|
2017-04-11 09:26:19 +08:00
|
|
|
{
|
|
|
|
Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", found, desired),
|
2016-08-23 08:31:50 +08:00
|
|
|
F: func() error {
|
2017-10-03 23:53:56 +08:00
|
|
|
<-throttle
|
2016-08-23 08:31:50 +08:00
|
|
|
_, err := n.client.DomainDNSSetCustom(sld, tld, desired)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
return nil, nil
|
|
|
|
}
|