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  }