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  }