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