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 }