package dnsimple import ( "encoding/json" "fmt" "sort" "strconv" "strings" "github.com/StackExchange/dnscontrol/models" "github.com/StackExchange/dnscontrol/providers" "github.com/StackExchange/dnscontrol/providers/diff" "github.com/miekg/dns/dnsutil" dnsimpleapi "github.com/dnsimple/dnsimple-go/dnsimple" ) const stateRegistered = "registered" var defaultNameServerNames = []string{ "ns1.dnsimple.com", "ns2.dnsimple.com", "ns3.dnsimple.com", "ns4.dnsimple.com", } type DnsimpleApi struct { AccountToken string // The account access token BaseURL string // An alternate base URI accountId string // Account id cache } func (c *DnsimpleApi) GetNameservers(domainName string) ([]*models.Nameserver, error) { return models.StringsToNameservers(defaultNameServerNames), nil } func (c *DnsimpleApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { corrections := []*models.Correction{} records, err := c.getRecords(dc.Name) if err != nil { return nil, err } var actual []*models.RecordConfig for _, r := range records { if r.Type == "SOA" || r.Type == "NS" { continue } if r.Name == "" { r.Name = "@" } if r.Type == "CNAME" || r.Type == "MX" { r.Content += "." } rec := &models.RecordConfig{ NameFQDN: dnsutil.AddOrigin(r.Name, dc.Name), Type: r.Type, Target: r.Content, TTL: uint32(r.TTL), Priority: uint16(r.Priority), Original: r, } actual = append(actual, rec) } removeOtherNS(dc) differ := diff.New(dc) _, create, delete, modify := differ.IncrementalDiff(actual) for _, del := range delete { rec := del.Existing.Original.(dnsimpleapi.ZoneRecord) corrections = append(corrections, &models.Correction{ Msg: del.String(), F: c.deleteRecordFunc(rec.ID, dc.Name), }) } for _, cre := range create { rec := cre.Desired corrections = append(corrections, &models.Correction{ Msg: cre.String(), F: c.createRecordFunc(rec, dc.Name), }) } for _, mod := range modify { old := mod.Existing.Original.(dnsimpleapi.ZoneRecord) new := mod.Desired corrections = append(corrections, &models.Correction{ Msg: mod.String(), F: c.updateRecordFunc(&old, new, dc.Name), }) } return corrections, nil } func (c *DnsimpleApi) GetRegistrarCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { corrections := []*models.Correction{} nameServers, err := c.getNameservers(dc.Name) if err != nil { return nil, err } actual := strings.Join(nameServers, ",") expectedSet := []string{} for _, ns := range dc.Nameservers { expectedSet = append(expectedSet, ns.Name) } sort.Strings(expectedSet) expected := strings.Join(expectedSet, ",") if actual != expected { return []*models.Correction{ { Msg: fmt.Sprintf("Update nameservers %s -> %s", actual, expected), F: c.updateNameserversFunc(expectedSet, dc.Name), }, }, nil } return corrections, nil } // DNSimple calls func (c *DnsimpleApi) getClient() *dnsimpleapi.Client { client := dnsimpleapi.NewClient(dnsimpleapi.NewOauthTokenCredentials(c.AccountToken)) if c.BaseURL != "" { client.BaseURL = c.BaseURL } return client } func (c *DnsimpleApi) getAccountId() (string, error) { if c.accountId == "" { client := c.getClient() whoamiResponse, err := client.Identity.Whoami() if err != nil { return "", err } if whoamiResponse.Data.User != nil && whoamiResponse.Data.Account == nil { return "", fmt.Errorf("DNSimple token appears to be a user token. Please supply an account token") } c.accountId = strconv.Itoa(whoamiResponse.Data.Account.ID) } return c.accountId, nil } func (c *DnsimpleApi) getRecords(domainName string) ([]dnsimpleapi.ZoneRecord, error) { client := c.getClient() accountId, err := c.getAccountId() if err != nil { return nil, err } recordsResponse, err := client.Zones.ListRecords(accountId, domainName, nil) if err != nil { return nil, err } return recordsResponse.Data, nil } // Returns the name server names that should be used. If the domain is registered // then this method will return the delegation name servers. If this domain // is hosted only, then it will return the default DNSimple name servers. func (c *DnsimpleApi) getNameservers(domainName string) ([]string, error) { client := c.getClient() accountId, err := c.getAccountId() if err != nil { return nil, err } domainResponse, err := client.Domains.GetDomain(accountId, domainName) if err != nil { return nil, err } if domainResponse.Data.State == stateRegistered { delegationResponse, err := client.Registrar.GetDomainDelegation(accountId, domainName) if err != nil { return nil, err } return *delegationResponse.Data, nil } else { return defaultNameServerNames, nil } } // Returns a function that can be invoked to change the delegation of the domain to the given name server names. func (c *DnsimpleApi) updateNameserversFunc(nameServerNames []string, domainName string) func() error { return func() error { client := c.getClient() accountId, err := c.getAccountId() if err != nil { return err } nameServers := dnsimpleapi.Delegation(nameServerNames) _, err = client.Registrar.ChangeDomainDelegation(accountId, domainName, &nameServers) if err != nil { return err } return nil } } // Returns a function that can be invoked to create a record in a zone. func (c *DnsimpleApi) createRecordFunc(rc *models.RecordConfig, domainName string) func() error { return func() error { client := c.getClient() accountId, err := c.getAccountId() if err != nil { return err } record := dnsimpleapi.ZoneRecord{ Name: dnsutil.TrimDomainName(rc.NameFQDN, domainName), Type: rc.Type, Content: rc.Target, TTL: int(rc.TTL), Priority: int(rc.Priority), } _, err = client.Zones.CreateRecord(accountId, domainName, record) if err != nil { return err } return nil } } // Returns a function that can be invoked to delete a record in a zone. func (c *DnsimpleApi) deleteRecordFunc(recordId int, domainName string) func() error { return func() error { client := c.getClient() accountId, err := c.getAccountId() if err != nil { return err } _, err = client.Zones.DeleteRecord(accountId, domainName, recordId) if err != nil { return err } return nil } } // Returns a function that can be invoked to update a record in a zone. func (c *DnsimpleApi) updateRecordFunc(old *dnsimpleapi.ZoneRecord, rc *models.RecordConfig, domainName string) func() error { return func() error { client := c.getClient() accountId, err := c.getAccountId() if err != nil { return err } record := dnsimpleapi.ZoneRecord{ Name: dnsutil.TrimDomainName(rc.NameFQDN, domainName), Type: rc.Type, Content: rc.Target, TTL: int(rc.TTL), Priority: int(rc.Priority), } _, err = client.Zones.UpdateRecord(accountId, domainName, old.ID, record) if err != nil { return err } return nil } } // constructors func newReg(conf map[string]string) (providers.Registrar, error) { return newProvider(conf, nil) } func newDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { return newProvider(conf, metadata) } func newProvider(m map[string]string, metadata json.RawMessage) (*DnsimpleApi, error) { api := &DnsimpleApi{} api.AccountToken = m["token"] if api.AccountToken == "" { return nil, fmt.Errorf("DNSimple token must be provided.") } if m["baseurl"] != "" { api.BaseURL = m["baseurl"] } return api, nil } func init() { providers.RegisterRegistrarType("DNSIMPLE", newReg) providers.RegisterDomainServiceProviderType("DNSIMPLE", newDsp) } // remove all non-dnsimple NS records from our desired state. // if any are found, print a warning func removeOtherNS(dc *models.DomainConfig) { newList := make([]*models.RecordConfig, 0, len(dc.Records)) for _, rec := range dc.Records { if rec.Type == "NS" { // apex NS inside dnsimple are expected. if rec.NameFQDN == dc.Name && strings.HasSuffix(rec.Target, ".dnsimple.com.") { continue } fmt.Printf("Warning: dnsimple.com does not allow NS records to be modified. %s will not be added.\n", rec.Target) continue } newList = append(newList, rec) } dc.Records = newList }