github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/namecheap/namecheapProvider.go (about) 1 package namecheap 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "sort" 7 "strings" 8 "time" 9 10 nc "github.com/billputer/go-namecheap" 11 "golang.org/x/net/publicsuffix" 12 13 "github.com/StackExchange/dnscontrol/v2/models" 14 "github.com/StackExchange/dnscontrol/v2/pkg/printer" 15 "github.com/StackExchange/dnscontrol/v2/providers" 16 "github.com/StackExchange/dnscontrol/v2/providers/diff" 17 ) 18 19 // NamecheapDefaultNs lists the default nameservers for this provider. 20 var NamecheapDefaultNs = []string{"dns1.registrar-servers.com", "dns2.registrar-servers.com"} 21 22 // Namecheap is the handle for this provider. 23 type Namecheap struct { 24 ApiKey string 25 ApiUser string 26 client *nc.Client 27 } 28 29 var features = providers.DocumentationNotes{ 30 providers.CanUseAlias: providers.Cannot(), 31 providers.CanUseCAA: providers.Can(), 32 providers.CanUsePTR: providers.Cannot(), 33 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"), 34 providers.CanUseTLSA: providers.Cannot(), 35 providers.CantUseNOPURGE: providers.Cannot(), 36 providers.DocCreateDomains: providers.Cannot("Requires domain registered through their service"), 37 providers.DocDualHost: providers.Cannot("Doesn't allow control of apex NS records"), 38 providers.DocOfficiallySupported: providers.Cannot(), 39 providers.CanGetZones: providers.Unimplemented(), 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, fmt.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 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. 110 func (client *Namecheap) GetZoneRecords(domain string) (models.Records, error) { 111 return nil, fmt.Errorf("not implemented") 112 // This enables the get-zones subcommand. 113 // Implement this by extracting the code from GetDomainCorrections into 114 // a single function. For most providers this should be relatively easy. 115 } 116 117 // GetDomainCorrections returns the corrections for the domain. 118 func (n *Namecheap) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 119 dc.Punycode() 120 sld, tld := splitDomain(dc.Name) 121 var records *nc.DomainDNSGetHostsResult 122 var err error 123 doWithRetry(func() error { 124 records, err = n.client.DomainsDNSGetHosts(sld, tld) 125 return err 126 }) 127 if err != nil { 128 return nil, err 129 } 130 131 var actual []*models.RecordConfig 132 133 // namecheap does not allow setting @ NS with basic DNS 134 dc.Filter(func(r *models.RecordConfig) bool { 135 if r.Type == "NS" && r.GetLabel() == "@" { 136 if !strings.HasSuffix(r.GetTargetField(), "registrar-servers.com.") { 137 fmt.Println("\n", r.GetTargetField(), "Namecheap does not support changing apex NS records. Skipping.") 138 } 139 return false 140 } 141 return true 142 }) 143 144 // namecheap has this really annoying feature where they add some parking records if you have no records. 145 // This causes a few problems for our purposes, specifically the integration tests. 146 // lets detect that one case and pretend it is a no-op. 147 if len(dc.Records) == 0 && len(records.Hosts) == 2 { 148 if records.Hosts[0].Type == "CNAME" && 149 strings.Contains(records.Hosts[0].Address, "parkingpage") && 150 records.Hosts[1].Type == "URL" { 151 return nil, nil 152 } 153 } 154 155 for _, r := range records.Hosts { 156 if r.Type == "SOA" { 157 continue 158 } 159 rec := &models.RecordConfig{ 160 Type: r.Type, 161 TTL: uint32(r.TTL), 162 MxPreference: uint16(r.MXPref), 163 Original: r, 164 } 165 rec.SetLabel(r.Name, dc.Name) 166 switch rtype := r.Type; rtype { // #rtype_variations 167 case "TXT": 168 rec.SetTargetTXT(r.Address) 169 case "CAA": 170 rec.SetTargetCAAString(r.Address) 171 default: 172 rec.SetTarget(r.Address) 173 } 174 actual = append(actual, rec) 175 } 176 177 // Normalize 178 models.PostProcessRecords(actual) 179 180 differ := diff.New(dc) 181 _, create, delete, modify := differ.IncrementalDiff(actual) 182 183 // // because namecheap doesn't have selective create, delete, modify, 184 // // we bundle them all up to send at once. We *do* want to see the 185 // // changes though 186 187 var desc []string 188 for _, i := range create { 189 desc = append(desc, "\n"+i.String()) 190 } 191 for _, i := range delete { 192 desc = append(desc, "\n"+i.String()) 193 } 194 for _, i := range modify { 195 desc = append(desc, "\n"+i.String()) 196 } 197 198 msg := fmt.Sprintf("GENERATE_ZONE: %s (%d records)%s", dc.Name, len(dc.Records), desc) 199 corrections := []*models.Correction{} 200 201 // only create corrections if there are changes 202 if len(desc) > 0 { 203 corrections = append(corrections, 204 &models.Correction{ 205 Msg: msg, 206 F: func() error { 207 return n.generateRecords(dc) 208 }, 209 }) 210 } 211 212 return corrections, nil 213 } 214 215 func (n *Namecheap) generateRecords(dc *models.DomainConfig) error { 216 217 var recs []nc.DomainDNSHost 218 219 id := 1 220 for _, r := range dc.Records { 221 var value string 222 switch rtype := r.Type; rtype { // #rtype_variations 223 case "CAA": 224 value = r.GetTargetCombined() 225 default: 226 value = r.GetTargetField() 227 } 228 229 rec := nc.DomainDNSHost{ 230 ID: id, 231 Name: r.GetLabel(), 232 Type: r.Type, 233 Address: value, 234 MXPref: int(r.MxPreference), 235 TTL: int(r.TTL), 236 } 237 recs = append(recs, rec) 238 id++ 239 } 240 sld, tld := splitDomain(dc.Name) 241 var err error 242 doWithRetry(func() error { 243 _, err = n.client.DomainDNSSetHosts(sld, tld, recs) 244 return err 245 }) 246 return err 247 } 248 249 // GetNameservers returns the nameservers for a domain. 250 func (n *Namecheap) GetNameservers(domainName string) ([]*models.Nameserver, error) { 251 // return default namecheap nameservers 252 ns := NamecheapDefaultNs 253 254 return models.StringsToNameservers(ns), nil 255 } 256 257 // GetRegistrarCorrections returns corrections to update nameservers. 258 func (n *Namecheap) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 259 var info *nc.DomainInfo 260 var err error 261 doWithRetry(func() error { 262 info, err = n.client.DomainGetInfo(dc.Name) 263 return err 264 }) 265 if err != nil { 266 return nil, err 267 } 268 sort.Strings(info.DNSDetails.Nameservers) 269 found := strings.Join(info.DNSDetails.Nameservers, ",") 270 desiredNs := []string{} 271 for _, d := range dc.Nameservers { 272 desiredNs = append(desiredNs, d.Name) 273 } 274 sort.Strings(desiredNs) 275 desired := strings.Join(desiredNs, ",") 276 if found != desired { 277 parts := strings.SplitN(dc.Name, ".", 2) 278 sld, tld := parts[0], parts[1] 279 return []*models.Correction{ 280 { 281 Msg: fmt.Sprintf("Change Nameservers from '%s' to '%s'", found, desired), 282 F: func() (err error) { 283 doWithRetry(func() error { 284 _, err = n.client.DomainDNSSetCustom(sld, tld, desired) 285 return err 286 }) 287 return 288 }}, 289 }, nil 290 } 291 return nil, nil 292 }