github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/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 dnsimpleapi "github.com/dnsimple/dnsimple-go/dnsimple" 12 "golang.org/x/oauth2" 13 14 "github.com/StackExchange/dnscontrol/v2/models" 15 "github.com/StackExchange/dnscontrol/v2/providers" 16 "github.com/StackExchange/dnscontrol/v2/providers/diff" 17 ) 18 19 var features = providers.DocumentationNotes{ 20 providers.CanUseAlias: providers.Can(), 21 providers.CanUseCAA: providers.Can(), 22 providers.CanUsePTR: providers.Can(), 23 providers.CanUseSSHFP: providers.Can(), 24 providers.CanUseSRV: providers.Can(), 25 providers.CanUseTXTMulti: providers.Can(), 26 providers.CanAutoDNSSEC: providers.Can(), 27 providers.CanUseTLSA: providers.Cannot(), 28 providers.DocCreateDomains: providers.Cannot(), 29 providers.DocDualHost: providers.Cannot("DNSimple does not allow sufficient control over the apex NS records"), 30 providers.DocOfficiallySupported: providers.Cannot(), 31 providers.CanGetZones: providers.Can(), 32 } 33 34 func init() { 35 providers.RegisterRegistrarType("DNSIMPLE", newReg) 36 providers.RegisterDomainServiceProviderType("DNSIMPLE", newDsp, features) 37 } 38 39 const stateRegistered = "registered" 40 41 var defaultNameServerNames = []string{ 42 "ns1.dnsimple.com", 43 "ns2.dnsimple.com", 44 "ns3.dnsimple.com", 45 "ns4.dnsimple.com", 46 } 47 48 // DnsimpleApi is the handle for this provider. 49 type DnsimpleApi struct { 50 AccountToken string // The account access token 51 BaseURL string // An alternate base URI 52 accountID string // Account id cache 53 } 54 55 // GetNameservers returns the name servers for a domain. 56 func (c *DnsimpleApi) GetNameservers(domainName string) ([]*models.Nameserver, error) { 57 return models.StringsToNameservers(defaultNameServerNames), nil 58 } 59 60 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. 61 func (client *DnsimpleApi) GetZoneRecords(domain string) (models.Records, error) { 62 records, err := client.getRecords(domain) 63 if err != nil { 64 return nil, err 65 } 66 67 var cleanedRecords models.Records 68 for _, r := range records { 69 if r.Type == "SOA" { 70 continue 71 } 72 if r.Name == "" { 73 r.Name = "@" 74 } 75 if r.Type == "CNAME" || r.Type == "MX" || r.Type == "ALIAS" { 76 r.Content += "." 77 } 78 // DNSimple adds TXT records that mirror the alias records. 79 // They manage them on ALIAS updates, so pretend they don't exist 80 if r.Type == "TXT" && strings.HasPrefix(r.Content, "ALIAS for ") { 81 continue 82 } 83 rec := &models.RecordConfig{ 84 TTL: uint32(r.TTL), 85 Original: r, 86 } 87 rec.SetLabel(r.Name, domain) 88 switch rtype := r.Type; rtype { 89 case "DNSKEY", "CDNSKEY", "CDS": 90 continue 91 case "ALIAS", "URL": 92 rec.Type = r.Type 93 rec.SetTarget(r.Content) 94 case "MX": 95 if err := rec.SetTargetMX(uint16(r.Priority), r.Content); err != nil { 96 panic(fmt.Errorf("unparsable record received from dnsimple: %w", err)) 97 } 98 case "SRV": 99 parts := strings.Fields(r.Content) 100 if len(parts) == 3 { 101 r.Content += "." 102 } 103 if err := rec.SetTargetSRVPriorityString(uint16(r.Priority), r.Content); err != nil { 104 panic(fmt.Errorf("unparsable record received from dnsimple: %w", err)) 105 } 106 default: 107 if err := rec.PopulateFromString(r.Type, r.Content, domain); err != nil { 108 panic(fmt.Errorf("unparsable record received from dnsimple: %w", err)) 109 } 110 } 111 cleanedRecords = append(cleanedRecords, rec) 112 } 113 114 return cleanedRecords, nil 115 } 116 117 // GetDomainCorrections returns corrections that update a domain. 118 func (c *DnsimpleApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 119 corrections := []*models.Correction{} 120 err := dc.Punycode() 121 if err != nil { 122 return nil, err 123 } 124 125 dnssecFixes, err := c.getDNSSECCorrections(dc) 126 if err != nil { 127 return nil, err 128 } 129 corrections = append(corrections, dnssecFixes...) 130 131 records, err := c.GetZoneRecords(dc.Name) 132 if err != nil { 133 return nil, err 134 } 135 actual := removeNS(records) 136 removeOtherNS(dc) 137 138 // Normalize 139 models.PostProcessRecords(actual) 140 141 differ := diff.New(dc) 142 _, create, del, modify := differ.IncrementalDiff(actual) 143 144 for _, del := range del { 145 rec := del.Existing.Original.(dnsimpleapi.ZoneRecord) 146 corrections = append(corrections, &models.Correction{ 147 Msg: del.String(), 148 F: c.deleteRecordFunc(rec.ID, dc.Name), 149 }) 150 } 151 152 for _, cre := range create { 153 rec := cre.Desired 154 corrections = append(corrections, &models.Correction{ 155 Msg: cre.String(), 156 F: c.createRecordFunc(rec, dc.Name), 157 }) 158 } 159 160 for _, mod := range modify { 161 old := mod.Existing.Original.(dnsimpleapi.ZoneRecord) 162 rec := mod.Desired 163 corrections = append(corrections, &models.Correction{ 164 Msg: mod.String(), 165 F: c.updateRecordFunc(&old, rec, dc.Name), 166 }) 167 } 168 169 return corrections, nil 170 } 171 172 func removeNS(records models.Records) models.Records { 173 var noNameServers models.Records 174 for _, r := range records { 175 if r.Type != "NS" { 176 noNameServers = append(noNameServers, r) 177 } 178 } 179 return noNameServers 180 } 181 182 // GetRegistrarCorrections returns corrections that update a domain's registrar. 183 func (c *DnsimpleApi) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 184 corrections := []*models.Correction{} 185 186 nameServers, err := c.getNameservers(dc.Name) 187 if err != nil { 188 return nil, err 189 } 190 sort.Strings(nameServers) 191 192 actual := strings.Join(nameServers, ",") 193 194 expectedSet := []string{} 195 for _, ns := range dc.Nameservers { 196 expectedSet = append(expectedSet, ns.Name) 197 } 198 sort.Strings(expectedSet) 199 expected := strings.Join(expectedSet, ",") 200 201 if actual != expected { 202 return []*models.Correction{ 203 { 204 Msg: fmt.Sprintf("Update nameservers %s -> %s", actual, expected), 205 F: c.updateNameserversFunc(expectedSet, dc.Name), 206 }, 207 }, nil 208 } 209 210 return corrections, nil 211 } 212 213 // getDNSSECCorrections returns corrections that update a domain's DNSSEC state. 214 func (c *DnsimpleApi) getDNSSECCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 215 enabled, err := c.getDnssec(dc.Name) 216 if err != nil { 217 return nil, err 218 } 219 220 if enabled && !dc.AutoDNSSEC { 221 return []*models.Correction{ 222 { 223 Msg: "Disable DNSSEC", 224 F: func() error { _, err := c.disableDnssec(dc.Name); return err }, 225 }, 226 }, nil 227 } 228 229 if !enabled && dc.AutoDNSSEC { 230 return []*models.Correction{ 231 { 232 Msg: "Enable DNSSEC", 233 F: func() error { _, err := c.enableDnssec(dc.Name); return err }, 234 }, 235 }, nil 236 } 237 238 return []*models.Correction{}, nil 239 } 240 241 // DNSimple calls 242 243 func (c *DnsimpleApi) getClient() *dnsimpleapi.Client { 244 ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.AccountToken}) 245 tc := oauth2.NewClient(context.Background(), ts) 246 247 // new client 248 client := dnsimpleapi.NewClient(tc) 249 250 if c.BaseURL != "" { 251 client.BaseURL = c.BaseURL 252 } 253 return client 254 } 255 256 func (c *DnsimpleApi) getAccountID() (string, error) { 257 if c.accountID == "" { 258 client := c.getClient() 259 whoamiResponse, err := client.Identity.Whoami() 260 if err != nil { 261 return "", err 262 } 263 if whoamiResponse.Data.User != nil && whoamiResponse.Data.Account == nil { 264 return "", fmt.Errorf("DNSimple token appears to be a user token. Please supply an account token") 265 } 266 c.accountID = strconv.FormatInt(whoamiResponse.Data.Account.ID, 10) 267 } 268 return c.accountID, nil 269 } 270 271 func (c *DnsimpleApi) getRecords(domainName string) ([]dnsimpleapi.ZoneRecord, error) { 272 client := c.getClient() 273 274 accountID, err := c.getAccountID() 275 if err != nil { 276 return nil, err 277 } 278 279 opts := &dnsimpleapi.ZoneRecordListOptions{} 280 recs := []dnsimpleapi.ZoneRecord{} 281 opts.Page = 1 282 for { 283 recordsResponse, err := client.Zones.ListRecords(accountID, domainName, opts) 284 if err != nil { 285 return nil, err 286 } 287 recs = append(recs, recordsResponse.Data...) 288 pg := recordsResponse.Pagination 289 if pg.CurrentPage == pg.TotalPages { 290 break 291 } 292 opts.Page++ 293 } 294 295 return recs, nil 296 } 297 298 func (c *DnsimpleApi) getDnssec(domainName string) (bool, error) { 299 var ( 300 client *dnsimpleapi.Client 301 accountID string 302 err error 303 ) 304 client = c.getClient() 305 if accountID, err = c.getAccountID(); err != nil { 306 return false, err 307 } 308 309 dnssecResponse, err := client.Domains.GetDnssec(accountID, domainName) 310 if err != nil { 311 return false, err 312 } 313 if dnssecResponse.Data == nil { 314 return false, nil 315 } 316 return dnssecResponse.Data.Enabled, nil 317 } 318 319 func (c *DnsimpleApi) enableDnssec(domainName string) (bool, error) { 320 var ( 321 client *dnsimpleapi.Client 322 accountID string 323 err error 324 ) 325 client = c.getClient() 326 if accountID, err = c.getAccountID(); err != nil { 327 return false, err 328 } 329 330 dnssecResponse, err := client.Domains.EnableDnssec(accountID, domainName) 331 if err != nil { 332 return false, err 333 } 334 if dnssecResponse.Data == nil { 335 return false, nil 336 } 337 return dnssecResponse.Data.Enabled, nil 338 } 339 340 func (c *DnsimpleApi) disableDnssec(domainName string) (bool, error) { 341 var ( 342 client *dnsimpleapi.Client 343 accountID string 344 err error 345 ) 346 client = c.getClient() 347 if accountID, err = c.getAccountID(); err != nil { 348 return false, err 349 } 350 351 dnssecResponse, err := client.Domains.DisableDnssec(accountID, domainName) 352 if err != nil { 353 return false, err 354 } 355 if dnssecResponse.Data == nil { 356 return false, nil 357 } 358 return dnssecResponse.Data.Enabled, nil 359 } 360 361 // Returns the name server names that should be used. If the domain is registered 362 // then this method will return the delegation name servers. If this domain 363 // is hosted only, then it will return the default DNSimple name servers. 364 func (c *DnsimpleApi) getNameservers(domainName string) ([]string, error) { 365 client := c.getClient() 366 367 accountID, err := c.getAccountID() 368 if err != nil { 369 return nil, err 370 } 371 372 domainResponse, err := client.Domains.GetDomain(accountID, domainName) 373 if err != nil { 374 return nil, err 375 } 376 377 if domainResponse.Data.State == stateRegistered { 378 379 delegationResponse, err := client.Registrar.GetDomainDelegation(accountID, domainName) 380 if err != nil { 381 return nil, err 382 } 383 384 return *delegationResponse.Data, nil 385 } 386 return defaultNameServerNames, nil 387 } 388 389 // Returns a function that can be invoked to change the delegation of the domain to the given name server names. 390 func (c *DnsimpleApi) updateNameserversFunc(nameServerNames []string, domainName string) func() error { 391 return func() error { 392 client := c.getClient() 393 394 accountID, err := c.getAccountID() 395 if err != nil { 396 return err 397 } 398 399 nameServers := dnsimpleapi.Delegation(nameServerNames) 400 401 _, err = client.Registrar.ChangeDomainDelegation(accountID, domainName, &nameServers) 402 if err != nil { 403 return err 404 } 405 406 return nil 407 } 408 } 409 410 // Returns a function that can be invoked to create a record in a zone. 411 func (c *DnsimpleApi) createRecordFunc(rc *models.RecordConfig, domainName string) func() error { 412 return func() error { 413 client := c.getClient() 414 415 accountID, err := c.getAccountID() 416 if err != nil { 417 return err 418 } 419 record := dnsimpleapi.ZoneRecord{ 420 Name: rc.GetLabel(), 421 Type: rc.Type, 422 Content: getTargetRecordContent(rc), 423 TTL: int(rc.TTL), 424 Priority: getTargetRecordPriority(rc), 425 } 426 _, err = client.Zones.CreateRecord(accountID, domainName, record) 427 if err != nil { 428 return err 429 } 430 431 return nil 432 } 433 } 434 435 // Returns a function that can be invoked to delete a record in a zone. 436 func (c *DnsimpleApi) deleteRecordFunc(recordID int64, domainName string) func() error { 437 return func() error { 438 client := c.getClient() 439 440 accountID, err := c.getAccountID() 441 if err != nil { 442 return err 443 } 444 445 _, err = client.Zones.DeleteRecord(accountID, domainName, recordID) 446 if err != nil { 447 return err 448 } 449 450 return nil 451 452 } 453 } 454 455 // Returns a function that can be invoked to update a record in a zone. 456 func (c *DnsimpleApi) updateRecordFunc(old *dnsimpleapi.ZoneRecord, rc *models.RecordConfig, domainName string) func() error { 457 return func() error { 458 client := c.getClient() 459 460 accountID, err := c.getAccountID() 461 if err != nil { 462 return err 463 } 464 465 record := dnsimpleapi.ZoneRecord{ 466 Name: rc.GetLabel(), 467 Type: rc.Type, 468 Content: getTargetRecordContent(rc), 469 TTL: int(rc.TTL), 470 Priority: getTargetRecordPriority(rc), 471 } 472 473 _, err = client.Zones.UpdateRecord(accountID, domainName, old.ID, record) 474 if err != nil { 475 return err 476 } 477 478 return nil 479 } 480 } 481 482 // ListZones returns all the zones in an account 483 func (c *DnsimpleApi) ListZones() ([]string, error) { 484 client := c.getClient() 485 accountID, err := c.getAccountID() 486 if err != nil { 487 return nil, err 488 } 489 490 var zones []string 491 opts := &dnsimpleapi.ZoneListOptions{} 492 opts.Page = 1 493 for { 494 zonesResponse, err := client.Zones.ListZones(accountID, opts) 495 if err != nil { 496 return nil, err 497 } 498 for _, zone := range zonesResponse.Data { 499 zones = append(zones, zone.Name) 500 } 501 pg := zonesResponse.Pagination 502 if pg.CurrentPage == pg.TotalPages { 503 break 504 } 505 opts.Page++ 506 } 507 return zones, nil 508 } 509 510 // constructors 511 512 func newReg(conf map[string]string) (providers.Registrar, error) { 513 return newProvider(conf, nil) 514 } 515 516 func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 517 return newProvider(conf, metadata) 518 } 519 520 func newProvider(m map[string]string, metadata json.RawMessage) (*DnsimpleApi, error) { 521 api := &DnsimpleApi{} 522 api.AccountToken = m["token"] 523 if api.AccountToken == "" { 524 return nil, fmt.Errorf("missing DNSimple token") 525 } 526 527 if m["baseurl"] != "" { 528 api.BaseURL = m["baseurl"] 529 } 530 531 return api, nil 532 } 533 534 // remove all non-dnsimple NS records from our desired state. 535 // if any are found, print a warning 536 func removeOtherNS(dc *models.DomainConfig) { 537 newList := make([]*models.RecordConfig, 0, len(dc.Records)) 538 for _, rec := range dc.Records { 539 if rec.Type == "NS" { 540 // apex NS inside dnsimple are expected. 541 if rec.GetLabelFQDN() == dc.Name && strings.HasSuffix(rec.GetTargetField(), ".dnsimple.com.") { 542 continue 543 } 544 fmt.Printf("Warning: dnsimple.com does not allow NS records to be modified. %s will not be added.\n", rec.GetTargetField()) 545 continue 546 } 547 newList = append(newList, rec) 548 } 549 dc.Records = newList 550 } 551 552 // Return the correct combined content for all special record types, Target for everything else 553 // Using RecordConfig.GetTargetCombined returns priority in the string, which we do not allow 554 func getTargetRecordContent(rc *models.RecordConfig) string { 555 switch rtype := rc.Type; rtype { 556 case "CAA": 557 return rc.GetTargetCombined() 558 case "SSHFP": 559 return fmt.Sprintf("%d %d %s", rc.SshfpAlgorithm, rc.SshfpFingerprint, rc.GetTargetField()) 560 case "SRV": 561 return fmt.Sprintf("%d %d %s", rc.SrvWeight, rc.SrvPort, rc.GetTargetField()) 562 case "TXT": 563 quoted := make([]string, len(rc.TxtStrings)) 564 for i := range rc.TxtStrings { 565 quoted[i] = quoteDNSString(rc.TxtStrings[i]) 566 } 567 return strings.Join(quoted, " ") 568 default: 569 return rc.GetTargetField() 570 } 571 } 572 573 // Return the correct priority for the record type, 0 for records without priority 574 func getTargetRecordPriority(rc *models.RecordConfig) int { 575 switch rtype := rc.Type; rtype { 576 case "MX": 577 return int(rc.MxPreference) 578 case "SRV": 579 return int(rc.SrvPriority) 580 default: 581 return 0 582 } 583 } 584 585 // Return a DNS string appropriately escaped for DNSimple. 586 // Should include the surrounding quotes. 587 // 588 // Warning: the DNSimple API is severely underdocumented in this area. 589 // I know that it takes multiple quoted strings just fine, and constructs the 590 // DNS multiple quoted items. 591 // I'm not 100% on the escaping, but since it's a JSON API, JSON escaping seems 592 // reasonable. 593 // I do know that DNSimple have their own checks, so anything too crazy will 594 // get a "400 Validation failed" HTTP response. 595 func quoteDNSString(unquoted string) string { 596 b, err := json.Marshal(unquoted) 597 if err != nil { 598 panic(fmt.Errorf("unable to marshal to JSON: %q", unquoted)) 599 } 600 return string(b) 601 }