github.com/hexonet/dnscontrol@v0.2.8/providers/dnsimple/dnsimpleProvider.go (about) 1 package dnsimple 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "sort" 8 "strconv" 9 "strings" 10 11 "github.com/StackExchange/dnscontrol/models" 12 "github.com/StackExchange/dnscontrol/providers" 13 "github.com/StackExchange/dnscontrol/providers/diff" 14 "github.com/pkg/errors" 15 "golang.org/x/oauth2" 16 17 dnsimpleapi "github.com/dnsimple/dnsimple-go/dnsimple" 18 ) 19 20 var features = providers.DocumentationNotes{ 21 providers.CanUseAlias: providers.Can(), 22 providers.CanUseCAA: providers.Can(), 23 providers.CanUsePTR: providers.Can(), 24 providers.CanUseSRV: providers.Can(), 25 providers.CanUseTLSA: providers.Cannot(), 26 providers.DocCreateDomains: providers.Cannot(), 27 providers.DocDualHost: providers.Cannot("DNSimple does not allow sufficient control over the apex NS records"), 28 providers.DocOfficiallySupported: providers.Cannot(), 29 } 30 31 func init() { 32 providers.RegisterRegistrarType("DNSIMPLE", newReg) 33 providers.RegisterDomainServiceProviderType("DNSIMPLE", newDsp, features) 34 } 35 36 const stateRegistered = "registered" 37 38 var defaultNameServerNames = []string{ 39 "ns1.dnsimple.com", 40 "ns2.dnsimple.com", 41 "ns3.dnsimple.com", 42 "ns4.dnsimple.com", 43 } 44 45 // DnsimpleApi is the handle for this provider. 46 type DnsimpleApi struct { 47 AccountToken string // The account access token 48 BaseURL string // An alternate base URI 49 accountID string // Account id cache 50 } 51 52 // GetNameservers returns the name servers for a domain. 53 func (c *DnsimpleApi) GetNameservers(domainName string) ([]*models.Nameserver, error) { 54 return models.StringsToNameservers(defaultNameServerNames), nil 55 } 56 57 // GetDomainCorrections returns corrections that update a domain. 58 func (c *DnsimpleApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 59 corrections := []*models.Correction{} 60 dc.Punycode() 61 records, err := c.getRecords(dc.Name) 62 if err != nil { 63 return nil, err 64 } 65 66 var actual []*models.RecordConfig 67 for _, r := range records { 68 if r.Type == "SOA" || r.Type == "NS" { 69 continue 70 } 71 if r.Name == "" { 72 r.Name = "@" 73 } 74 if r.Type == "CNAME" || r.Type == "MX" || r.Type == "ALIAS" || r.Type == "SRV" { 75 r.Content += "." 76 } 77 // dnsimple adds these odd txt records that mirror the alias records. 78 // they seem to manage them on deletes and things, so we'll just pretend they don't exist 79 if r.Type == "TXT" && strings.HasPrefix(r.Content, "ALIAS for ") { 80 continue 81 } 82 rec := &models.RecordConfig{ 83 TTL: uint32(r.TTL), 84 Original: r, 85 } 86 rec.SetLabel(r.Name, dc.Name) 87 switch rtype := r.Type; rtype { 88 case "ALIAS", "URL": 89 rec.Type = r.Type 90 rec.SetTarget(r.Content) 91 case "MX": 92 if err := rec.SetTargetMX(uint16(r.Priority), r.Content); err != nil { 93 panic(errors.Wrap(err, "unparsable record received from dnsimple")) 94 } 95 case "SRV": 96 if err := rec.SetTargetSRVPriorityString(uint16(r.Priority), r.Content); err != nil { 97 panic(errors.Wrap(err, "unparsable record received from dnsimple")) 98 } 99 default: 100 if err := rec.PopulateFromString(r.Type, r.Content, dc.Name); err != nil { 101 panic(errors.Wrap(err, "unparsable record received from dnsimple")) 102 } 103 } 104 actual = append(actual, rec) 105 } 106 removeOtherNS(dc) 107 108 // Normalize 109 models.PostProcessRecords(actual) 110 111 differ := diff.New(dc) 112 _, create, del, modify := differ.IncrementalDiff(actual) 113 114 for _, del := range del { 115 rec := del.Existing.Original.(dnsimpleapi.ZoneRecord) 116 corrections = append(corrections, &models.Correction{ 117 Msg: del.String(), 118 F: c.deleteRecordFunc(rec.ID, dc.Name), 119 }) 120 } 121 122 for _, cre := range create { 123 rec := cre.Desired 124 corrections = append(corrections, &models.Correction{ 125 Msg: cre.String(), 126 F: c.createRecordFunc(rec, dc.Name), 127 }) 128 } 129 130 for _, mod := range modify { 131 old := mod.Existing.Original.(dnsimpleapi.ZoneRecord) 132 rec := mod.Desired 133 corrections = append(corrections, &models.Correction{ 134 Msg: mod.String(), 135 F: c.updateRecordFunc(&old, rec, dc.Name), 136 }) 137 } 138 139 return corrections, nil 140 } 141 142 // GetRegistrarCorrections returns corrections that update a domain's registrar. 143 func (c *DnsimpleApi) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 144 corrections := []*models.Correction{} 145 146 nameServers, err := c.getNameservers(dc.Name) 147 if err != nil { 148 return nil, err 149 } 150 sort.Strings(nameServers) 151 152 actual := strings.Join(nameServers, ",") 153 154 expectedSet := []string{} 155 for _, ns := range dc.Nameservers { 156 expectedSet = append(expectedSet, ns.Name) 157 } 158 sort.Strings(expectedSet) 159 expected := strings.Join(expectedSet, ",") 160 161 if actual != expected { 162 return []*models.Correction{ 163 { 164 Msg: fmt.Sprintf("Update nameservers %s -> %s", actual, expected), 165 F: c.updateNameserversFunc(expectedSet, dc.Name), 166 }, 167 }, nil 168 } 169 170 return corrections, nil 171 } 172 173 // DNSimple calls 174 175 func (c *DnsimpleApi) getClient() *dnsimpleapi.Client { 176 ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.AccountToken}) 177 tc := oauth2.NewClient(context.Background(), ts) 178 179 // new client 180 client := dnsimpleapi.NewClient(tc) 181 182 if c.BaseURL != "" { 183 client.BaseURL = c.BaseURL 184 } 185 return client 186 } 187 188 func (c *DnsimpleApi) getAccountID() (string, error) { 189 if c.accountID == "" { 190 client := c.getClient() 191 whoamiResponse, err := client.Identity.Whoami() 192 if err != nil { 193 return "", err 194 } 195 if whoamiResponse.Data.User != nil && whoamiResponse.Data.Account == nil { 196 return "", errors.Errorf("DNSimple token appears to be a user token. Please supply an account token") 197 } 198 c.accountID = strconv.FormatInt(whoamiResponse.Data.Account.ID, 10) 199 } 200 return c.accountID, nil 201 } 202 203 func (c *DnsimpleApi) getRecords(domainName string) ([]dnsimpleapi.ZoneRecord, error) { 204 client := c.getClient() 205 206 accountID, err := c.getAccountID() 207 if err != nil { 208 return nil, err 209 } 210 211 opts := &dnsimpleapi.ZoneRecordListOptions{} 212 recs := []dnsimpleapi.ZoneRecord{} 213 opts.Page = 1 214 for { 215 recordsResponse, err := client.Zones.ListRecords(accountID, domainName, opts) 216 if err != nil { 217 return nil, err 218 } 219 recs = append(recs, recordsResponse.Data...) 220 pg := recordsResponse.Pagination 221 if pg.CurrentPage == pg.TotalPages { 222 break 223 } 224 opts.Page++ 225 } 226 227 return recs, nil 228 } 229 230 // Returns the name server names that should be used. If the domain is registered 231 // then this method will return the delegation name servers. If this domain 232 // is hosted only, then it will return the default DNSimple name servers. 233 func (c *DnsimpleApi) getNameservers(domainName string) ([]string, error) { 234 client := c.getClient() 235 236 accountID, err := c.getAccountID() 237 if err != nil { 238 return nil, err 239 } 240 241 domainResponse, err := client.Domains.GetDomain(accountID, domainName) 242 if err != nil { 243 return nil, err 244 } 245 246 if domainResponse.Data.State == stateRegistered { 247 248 delegationResponse, err := client.Registrar.GetDomainDelegation(accountID, domainName) 249 if err != nil { 250 return nil, err 251 } 252 253 return *delegationResponse.Data, nil 254 } 255 return defaultNameServerNames, nil 256 } 257 258 // Returns a function that can be invoked to change the delegation of the domain to the given name server names. 259 func (c *DnsimpleApi) updateNameserversFunc(nameServerNames []string, domainName string) func() error { 260 return func() error { 261 client := c.getClient() 262 263 accountID, err := c.getAccountID() 264 if err != nil { 265 return err 266 } 267 268 nameServers := dnsimpleapi.Delegation(nameServerNames) 269 270 _, err = client.Registrar.ChangeDomainDelegation(accountID, domainName, &nameServers) 271 if err != nil { 272 return err 273 } 274 275 return nil 276 } 277 } 278 279 // Returns a function that can be invoked to create a record in a zone. 280 func (c *DnsimpleApi) createRecordFunc(rc *models.RecordConfig, domainName string) func() error { 281 return func() error { 282 client := c.getClient() 283 284 accountID, err := c.getAccountID() 285 if err != nil { 286 return err 287 } 288 record := dnsimpleapi.ZoneRecord{ 289 Name: rc.GetLabel(), 290 Type: rc.Type, 291 Content: getTargetRecordContent(rc), 292 TTL: int(rc.TTL), 293 Priority: getTargetRecordPriority(rc), 294 } 295 _, err = client.Zones.CreateRecord(accountID, domainName, record) 296 if err != nil { 297 return err 298 } 299 300 return nil 301 } 302 } 303 304 // Returns a function that can be invoked to delete a record in a zone. 305 func (c *DnsimpleApi) deleteRecordFunc(recordID int64, domainName string) func() error { 306 return func() error { 307 client := c.getClient() 308 309 accountID, err := c.getAccountID() 310 if err != nil { 311 return err 312 } 313 314 _, err = client.Zones.DeleteRecord(accountID, domainName, recordID) 315 if err != nil { 316 return err 317 } 318 319 return nil 320 321 } 322 } 323 324 // Returns a function that can be invoked to update a record in a zone. 325 func (c *DnsimpleApi) updateRecordFunc(old *dnsimpleapi.ZoneRecord, rc *models.RecordConfig, domainName string) func() error { 326 return func() error { 327 client := c.getClient() 328 329 accountID, err := c.getAccountID() 330 if err != nil { 331 return err 332 } 333 334 record := dnsimpleapi.ZoneRecord{ 335 Name: rc.GetLabel(), 336 Type: rc.Type, 337 Content: getTargetRecordContent(rc), 338 TTL: int(rc.TTL), 339 Priority: getTargetRecordPriority(rc), 340 } 341 342 _, err = client.Zones.UpdateRecord(accountID, domainName, old.ID, record) 343 if err != nil { 344 return err 345 } 346 347 return nil 348 } 349 } 350 351 // constructors 352 353 func newReg(conf map[string]string) (providers.Registrar, error) { 354 return newProvider(conf, nil) 355 } 356 357 func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 358 return newProvider(conf, metadata) 359 } 360 361 func newProvider(m map[string]string, metadata json.RawMessage) (*DnsimpleApi, error) { 362 api := &DnsimpleApi{} 363 api.AccountToken = m["token"] 364 if api.AccountToken == "" { 365 return nil, errors.Errorf("missing DNSimple token") 366 } 367 368 if m["baseurl"] != "" { 369 api.BaseURL = m["baseurl"] 370 } 371 372 return api, nil 373 } 374 375 // remove all non-dnsimple NS records from our desired state. 376 // if any are found, print a warning 377 func removeOtherNS(dc *models.DomainConfig) { 378 newList := make([]*models.RecordConfig, 0, len(dc.Records)) 379 for _, rec := range dc.Records { 380 if rec.Type == "NS" { 381 // apex NS inside dnsimple are expected. 382 if rec.GetLabelFQDN() == dc.Name && strings.HasSuffix(rec.GetTargetField(), ".dnsimple.com.") { 383 continue 384 } 385 fmt.Printf("Warning: dnsimple.com does not allow NS records to be modified. %s will not be added.\n", rec.GetTargetField()) 386 continue 387 } 388 newList = append(newList, rec) 389 } 390 dc.Records = newList 391 } 392 393 // Return the correct combined content for all special record types, Target for everything else 394 // Using RecordConfig.GetTargetCombined returns priority in the string, which we do not allow 395 func getTargetRecordContent(rc *models.RecordConfig) string { 396 switch rtype := rc.Type; rtype { 397 case "CAA": 398 return rc.GetTargetCombined() 399 case "SRV": 400 return fmt.Sprintf("%d %d %s", rc.SrvWeight, rc.SrvPort, rc.GetTargetField()) 401 default: 402 return rc.GetTargetField() 403 } 404 } 405 406 // Return the correct priority for the record type, 0 for records without priority 407 func getTargetRecordPriority(rc *models.RecordConfig) int { 408 switch rtype := rc.Type; rtype { 409 case "MX": 410 return int(rc.MxPreference) 411 case "SRV": 412 return int(rc.SrvPriority) 413 default: 414 return 0 415 } 416 }