mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-12 10:27:57 +08:00
b0f2945510
This should enable the diff2 code to be inserted with good "git blame" results for new code. I'm adding this early to catch any problems early.
332 lines
9.5 KiB
Go
332 lines
9.5 KiB
Go
package namecheap
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/diff2"
|
|
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
|
|
"github.com/StackExchange/dnscontrol/v3/providers"
|
|
nc "github.com/billputer/go-namecheap"
|
|
"golang.org/x/net/publicsuffix"
|
|
)
|
|
|
|
// NamecheapDefaultNs lists the default nameservers for this provider.
|
|
var NamecheapDefaultNs = []string{"dns1.registrar-servers.com", "dns2.registrar-servers.com"}
|
|
|
|
// namecheapProvider is the handle for this provider.
|
|
type namecheapProvider struct {
|
|
APIKEY string
|
|
APIUser string
|
|
client *nc.Client
|
|
}
|
|
|
|
var features = providers.DocumentationNotes{
|
|
providers.CanGetZones: providers.Can(),
|
|
providers.CanUseAlias: providers.Can(),
|
|
providers.CanUseCAA: providers.Can(),
|
|
providers.CanUsePTR: providers.Cannot(),
|
|
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"),
|
|
providers.CanUseTLSA: providers.Cannot(),
|
|
providers.CantUseNOPURGE: providers.Cannot(),
|
|
providers.DocCreateDomains: providers.Cannot("Requires domain registered through their service"),
|
|
providers.DocDualHost: providers.Cannot("Doesn't allow control of apex NS records"),
|
|
providers.DocOfficiallySupported: providers.Cannot(),
|
|
}
|
|
|
|
func init() {
|
|
providers.RegisterRegistrarType("NAMECHEAP", newReg)
|
|
fns := providers.DspFuncs{
|
|
Initializer: newDsp,
|
|
RecordAuditor: AuditRecords,
|
|
}
|
|
providers.RegisterDomainServiceProviderType("NAMECHEAP", fns, features)
|
|
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) (*namecheapProvider, error) {
|
|
api := &namecheapProvider{}
|
|
api.APIUser, api.APIKEY = m["apiuser"], m["apikey"]
|
|
if api.APIKEY == "" || api.APIUser == "" {
|
|
return nil, fmt.Errorf("missing Namecheap apikey and apiuser")
|
|
}
|
|
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
|
|
// 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."
|
|
//
|
|
// this helper performs some api action, checks for rate limited response, and if so, enters a retry loop until it resolves
|
|
// if you are consistently hitting this, you may have success asking their support to increase your account's limits.
|
|
func doWithRetry(f func() error) {
|
|
// sleep 5 seconds at a time, up to 23 times (1 minute, 15 seconds)
|
|
const maxRetries = 23
|
|
const sleepTime = 5 * time.Second
|
|
var currentRetry int
|
|
for {
|
|
err := f()
|
|
if err == nil {
|
|
return
|
|
}
|
|
if strings.Contains(err.Error(), "Error 500000: Too many requests") {
|
|
currentRetry++
|
|
if currentRetry >= maxRetries {
|
|
return
|
|
}
|
|
printer.Printf("Namecheap rate limit exceeded. Waiting %s to retry.\n", sleepTime)
|
|
time.Sleep(sleepTime)
|
|
} else {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
|
func (n *namecheapProvider) GetZoneRecords(domain string) (models.Records, error) {
|
|
sld, tld := splitDomain(domain)
|
|
var records *nc.DomainDNSGetHostsResult
|
|
var err error
|
|
doWithRetry(func() error {
|
|
records, err = n.client.DomainsDNSGetHosts(sld, tld)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return toRecords(records, domain)
|
|
}
|
|
|
|
// GetDomainCorrections returns the corrections for the domain.
|
|
func (n *namecheapProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
dc.Punycode()
|
|
sld, tld := splitDomain(dc.Name)
|
|
var records *nc.DomainDNSGetHostsResult
|
|
var err error
|
|
doWithRetry(func() error {
|
|
records, err = n.client.DomainsDNSGetHosts(sld, tld)
|
|
return err
|
|
})
|
|
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.GetLabel() == "@" {
|
|
if !strings.HasSuffix(r.GetTargetField(), "registrar-servers.com.") {
|
|
printer.Println("\n", r.GetTargetField(), "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{
|
|
Type: r.Type,
|
|
TTL: uint32(r.TTL),
|
|
MxPreference: uint16(r.MXPref),
|
|
Original: r,
|
|
}
|
|
rec.SetLabel(r.Name, dc.Name)
|
|
switch rtype := r.Type; rtype { // #rtype_variations
|
|
case "TXT":
|
|
rec.SetTargetTXT(r.Address)
|
|
case "CAA":
|
|
rec.SetTargetCAAString(r.Address)
|
|
default:
|
|
rec.SetTarget(r.Address)
|
|
}
|
|
actual = append(actual, rec)
|
|
}
|
|
|
|
// Normalize
|
|
models.PostProcessRecords(actual)
|
|
|
|
var corrections []*models.Correction
|
|
if !diff2.EnableDiff2 || true { // Remove "|| true" when diff2 version arrives
|
|
|
|
differ := diff.New(dc)
|
|
_, create, delete, modify, err := differ.IncrementalDiff(actual)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// // 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
|
|
}
|
|
|
|
// Insert Future diff2 version here.
|
|
|
|
return corrections, nil
|
|
}
|
|
|
|
func toRecords(result *nc.DomainDNSGetHostsResult, origin string) ([]*models.RecordConfig, error) {
|
|
var records []*models.RecordConfig
|
|
for _, dnsHost := range result.Hosts {
|
|
record := models.RecordConfig{
|
|
Type: dnsHost.Type,
|
|
TTL: uint32(dnsHost.TTL),
|
|
MxPreference: uint16(dnsHost.MXPref),
|
|
Name: dnsHost.Name,
|
|
}
|
|
record.PopulateFromString(dnsHost.Type, dnsHost.Address, origin)
|
|
|
|
records = append(records, &record)
|
|
}
|
|
|
|
return records, nil
|
|
}
|
|
|
|
func (n *namecheapProvider) generateRecords(dc *models.DomainConfig) error {
|
|
|
|
var recs []nc.DomainDNSHost
|
|
|
|
id := 1
|
|
for _, r := range dc.Records {
|
|
var value string
|
|
switch rtype := r.Type; rtype { // #rtype_variations
|
|
case "CAA":
|
|
value = r.GetTargetCombined()
|
|
default:
|
|
value = r.GetTargetField()
|
|
}
|
|
|
|
rec := nc.DomainDNSHost{
|
|
ID: id,
|
|
Name: r.GetLabel(),
|
|
Type: r.Type,
|
|
Address: value,
|
|
MXPref: int(r.MxPreference),
|
|
TTL: int(r.TTL),
|
|
}
|
|
recs = append(recs, rec)
|
|
id++
|
|
}
|
|
sld, tld := splitDomain(dc.Name)
|
|
var err error
|
|
doWithRetry(func() error {
|
|
_, err = n.client.DomainDNSSetHosts(sld, tld, recs)
|
|
return err
|
|
})
|
|
return err
|
|
}
|
|
|
|
// GetNameservers returns the nameservers for a domain.
|
|
func (n *namecheapProvider) GetNameservers(domainName string) ([]*models.Nameserver, error) {
|
|
// return default namecheap nameservers
|
|
return models.ToNameservers(NamecheapDefaultNs)
|
|
}
|
|
|
|
// GetRegistrarCorrections returns corrections to update nameservers.
|
|
func (n *namecheapProvider) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
|
|
var info *nc.DomainInfo
|
|
var err error
|
|
doWithRetry(func() error {
|
|
info, err = n.client.DomainGetInfo(dc.Name)
|
|
return err
|
|
})
|
|
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() (err error) {
|
|
doWithRetry(func() error {
|
|
_, err = n.client.DomainDNSSetCustom(sld, tld, desired)
|
|
return err
|
|
})
|
|
return
|
|
}},
|
|
}, nil
|
|
}
|
|
return nil, nil
|
|
}
|