github.com/kurthockenbury/dnscontrol@v0.2.8/providers/cloudflare/cloudflareProvider.go (about)

     1  package cloudflare
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"log"
     7  	"net"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/StackExchange/dnscontrol/models"
    12  	"github.com/StackExchange/dnscontrol/pkg/printer"
    13  	"github.com/StackExchange/dnscontrol/pkg/transform"
    14  	"github.com/StackExchange/dnscontrol/providers"
    15  	"github.com/StackExchange/dnscontrol/providers/diff"
    16  	"github.com/miekg/dns/dnsutil"
    17  	"github.com/pkg/errors"
    18  )
    19  
    20  /*
    21  
    22  Cloudflare API DNS provider:
    23  
    24  Info required in `creds.json`:
    25     - apikey
    26     - apiuser
    27     - accountid (optional)
    28     - accountname (optional)
    29  
    30  Record level metadata available:
    31     - cloudflare_proxy ("on", "off", or "full")
    32  
    33  Domain level metadata available:
    34     - cloudflare_proxy_default ("on", "off", or "full")
    35  
    36   Provider level metadata available:
    37     - ip_conversions
    38  */
    39  
    40  var features = providers.DocumentationNotes{
    41  	providers.CanUseAlias:            providers.Can("CF automatically flattens CNAME records into A records dynamically"),
    42  	providers.CanUseCAA:              providers.Can(),
    43  	providers.CanUseSRV:              providers.Can(),
    44  	providers.DocCreateDomains:       providers.Can(),
    45  	providers.DocDualHost:            providers.Cannot("Cloudflare will not work well in situations where it is not the only DNS server"),
    46  	providers.DocOfficiallySupported: providers.Can(),
    47  }
    48  
    49  func init() {
    50  	providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", newCloudflare, features)
    51  	providers.RegisterCustomRecordType("CF_REDIRECT", "CLOUDFLAREAPI", "")
    52  	providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", "CLOUDFLAREAPI", "")
    53  }
    54  
    55  // CloudflareApi is the handle for API calls.
    56  type CloudflareApi struct {
    57  	ApiKey          string `json:"apikey"`
    58  	ApiUser         string `json:"apiuser"`
    59  	AccountID       string `json:"accountid"`
    60  	AccountName     string `json:"accountname"`
    61  	domainIndex     map[string]string
    62  	nameservers     map[string][]string
    63  	ipConversions   []transform.IpConversion
    64  	ignoredLabels   []string
    65  	manageRedirects bool
    66  }
    67  
    68  func labelMatches(label string, matches []string) bool {
    69  	printer.Debugf("DEBUG: labelMatches(%#v, %#v)\n", label, matches)
    70  	for _, tst := range matches {
    71  		if label == tst {
    72  			return true
    73  		}
    74  	}
    75  	return false
    76  }
    77  
    78  // GetNameservers returns the nameservers for a domain.
    79  func (c *CloudflareApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
    80  	if c.domainIndex == nil {
    81  		if err := c.fetchDomainList(); err != nil {
    82  			return nil, err
    83  		}
    84  	}
    85  	ns, ok := c.nameservers[domain]
    86  	if !ok {
    87  		return nil, errors.Errorf("Nameservers for %s not found in cloudflare account", domain)
    88  	}
    89  	return models.StringsToNameservers(ns), nil
    90  }
    91  
    92  // GetDomainCorrections returns a list of corrections to update a domain.
    93  func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
    94  	if c.domainIndex == nil {
    95  		if err := c.fetchDomainList(); err != nil {
    96  			return nil, err
    97  		}
    98  	}
    99  	id, ok := c.domainIndex[dc.Name]
   100  	if !ok {
   101  		return nil, errors.Errorf("%s not listed in zones for cloudflare account", dc.Name)
   102  	}
   103  	if err := c.preprocessConfig(dc); err != nil {
   104  		return nil, err
   105  	}
   106  	records, err := c.getRecordsForDomain(id, dc.Name)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	for i := len(records) - 1; i >= 0; i-- {
   111  		rec := records[i]
   112  		// Delete ignore labels
   113  		if labelMatches(dnsutil.TrimDomainName(rec.Original.(*cfRecord).Name, dc.Name), c.ignoredLabels) {
   114  			printer.Debugf("ignored_label: %s\n", rec.Original.(*cfRecord).Name)
   115  			records = append(records[:i], records[i+1:]...)
   116  		}
   117  	}
   118  	if c.manageRedirects {
   119  		prs, err := c.getPageRules(id, dc.Name)
   120  		if err != nil {
   121  			return nil, err
   122  		}
   123  		records = append(records, prs...)
   124  	}
   125  	for _, rec := range dc.Records {
   126  		if rec.Type == "ALIAS" {
   127  			rec.Type = "CNAME"
   128  		}
   129  		if labelMatches(rec.GetLabel(), c.ignoredLabels) {
   130  			log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.GetLabel(), c.ignoredLabels)
   131  		}
   132  	}
   133  	checkNSModifications(dc)
   134  
   135  	// Normalize
   136  	models.PostProcessRecords(records)
   137  
   138  	differ := diff.New(dc, getProxyMetadata)
   139  	_, create, del, mod := differ.IncrementalDiff(records)
   140  	corrections := []*models.Correction{}
   141  
   142  	for _, d := range del {
   143  		ex := d.Existing
   144  		if ex.Type == "PAGE_RULE" {
   145  			corrections = append(corrections, &models.Correction{
   146  				Msg: d.String(),
   147  				F:   func() error { return c.deletePageRule(ex.Original.(*pageRule).ID, id) },
   148  			})
   149  
   150  		} else {
   151  			corrections = append(corrections, c.deleteRec(ex.Original.(*cfRecord), id))
   152  		}
   153  	}
   154  	for _, d := range create {
   155  		des := d.Desired
   156  		if des.Type == "PAGE_RULE" {
   157  			corrections = append(corrections, &models.Correction{
   158  				Msg: d.String(),
   159  				F:   func() error { return c.createPageRule(id, des.GetTargetField()) },
   160  			})
   161  		} else {
   162  			corrections = append(corrections, c.createRec(des, id)...)
   163  		}
   164  	}
   165  
   166  	for _, d := range mod {
   167  		rec := d.Desired
   168  		ex := d.Existing
   169  		if rec.Type == "PAGE_RULE" {
   170  			corrections = append(corrections, &models.Correction{
   171  				Msg: d.String(),
   172  				F:   func() error { return c.updatePageRule(ex.Original.(*pageRule).ID, id, rec.GetTargetField()) },
   173  			})
   174  		} else {
   175  			e := ex.Original.(*cfRecord)
   176  			proxy := e.Proxiable && rec.Metadata[metaProxy] != "off"
   177  			corrections = append(corrections, &models.Correction{
   178  				Msg: d.String(),
   179  				F:   func() error { return c.modifyRecord(id, e.ID, proxy, rec) },
   180  			})
   181  		}
   182  	}
   183  	return corrections, nil
   184  }
   185  
   186  func checkNSModifications(dc *models.DomainConfig) {
   187  	newList := make([]*models.RecordConfig, 0, len(dc.Records))
   188  	for _, rec := range dc.Records {
   189  		if rec.Type == "NS" && rec.GetLabelFQDN() == dc.Name {
   190  			if !strings.HasSuffix(rec.GetTargetField(), ".ns.cloudflare.com.") {
   191  				printer.Warnf("cloudflare does not support modifying NS records on base domain. %s will not be added.\n", rec.GetTargetField())
   192  			}
   193  			continue
   194  		}
   195  		newList = append(newList, rec)
   196  	}
   197  	dc.Records = newList
   198  }
   199  
   200  const (
   201  	metaProxy         = "cloudflare_proxy"
   202  	metaProxyDefault  = metaProxy + "_default"
   203  	metaOriginalIP    = "original_ip"    // TODO(tlim): Unclear what this means.
   204  	metaIPConversions = "ip_conversions" // TODO(tlim): Rename to obscure_rules.
   205  )
   206  
   207  func checkProxyVal(v string) (string, error) {
   208  	v = strings.ToLower(v)
   209  	if v != "on" && v != "off" && v != "full" {
   210  		return "", errors.Errorf("Bad metadata value for cloudflare_proxy: '%s'. Use on/off/full", v)
   211  	}
   212  	return v, nil
   213  }
   214  
   215  func (c *CloudflareApi) preprocessConfig(dc *models.DomainConfig) error {
   216  
   217  	// Determine the default proxy setting.
   218  	var defProxy string
   219  	var err error
   220  	if defProxy = dc.Metadata[metaProxyDefault]; defProxy == "" {
   221  		defProxy = "off"
   222  	} else {
   223  		defProxy, err = checkProxyVal(defProxy)
   224  		if err != nil {
   225  			return err
   226  		}
   227  	}
   228  
   229  	currentPrPrio := 1
   230  
   231  	// Normalize the proxy setting for each record.
   232  	// A and CNAMEs: Validate. If null, set to default.
   233  	// else: Make sure it wasn't set.  Set to default.
   234  	// iterate backwards so first defined page rules have highest priority
   235  	for i := len(dc.Records) - 1; i >= 0; i-- {
   236  		rec := dc.Records[i]
   237  		if rec.Metadata == nil {
   238  			rec.Metadata = map[string]string{}
   239  		}
   240  		if rec.TTL == 0 || rec.TTL == 300 {
   241  			rec.TTL = 1
   242  		}
   243  		if rec.TTL != 1 && rec.TTL < 120 {
   244  			rec.TTL = 120
   245  		}
   246  		if rec.Type != "A" && rec.Type != "CNAME" && rec.Type != "AAAA" && rec.Type != "ALIAS" {
   247  			if rec.Metadata[metaProxy] != "" {
   248  				return errors.Errorf("cloudflare_proxy set on %v record: %#v cloudflare_proxy=%#v", rec.Type, rec.GetLabel(), rec.Metadata[metaProxy])
   249  			}
   250  			// Force it to off.
   251  			rec.Metadata[metaProxy] = "off"
   252  		} else {
   253  			if val := rec.Metadata[metaProxy]; val == "" {
   254  				rec.Metadata[metaProxy] = defProxy
   255  			} else {
   256  				val, err := checkProxyVal(val)
   257  				if err != nil {
   258  					return err
   259  				}
   260  				rec.Metadata[metaProxy] = val
   261  			}
   262  		}
   263  		// CF_REDIRECT record types. Encode target as $FROM,$TO,$PRIO,$CODE
   264  		if rec.Type == "CF_REDIRECT" || rec.Type == "CF_TEMP_REDIRECT" {
   265  			if !c.manageRedirects {
   266  				return errors.Errorf("you must add 'manage_redirects: true' metadata to cloudflare provider to use CF_REDIRECT records")
   267  			}
   268  			parts := strings.Split(rec.GetTargetField(), ",")
   269  			if len(parts) != 2 {
   270  				return errors.Errorf("Invalid data specified for cloudflare redirect record")
   271  			}
   272  			code := 301
   273  			if rec.Type == "CF_TEMP_REDIRECT" {
   274  				code = 302
   275  			}
   276  			rec.SetTarget(fmt.Sprintf("%s,%d,%d", rec.GetTargetField(), currentPrPrio, code))
   277  			currentPrPrio++
   278  			rec.Type = "PAGE_RULE"
   279  		}
   280  	}
   281  
   282  	// look for ip conversions and transform records
   283  	for _, rec := range dc.Records {
   284  		if rec.Type != "A" {
   285  			continue
   286  		}
   287  		// only transform "full"
   288  		if rec.Metadata[metaProxy] != "full" {
   289  			continue
   290  		}
   291  		ip := net.ParseIP(rec.GetTargetField())
   292  		if ip == nil {
   293  			return errors.Errorf("%s is not a valid ip address", rec.GetTargetField())
   294  		}
   295  		newIP, err := transform.TransformIP(ip, c.ipConversions)
   296  		if err != nil {
   297  			return err
   298  		}
   299  		rec.Metadata[metaOriginalIP] = rec.GetTargetField()
   300  		rec.SetTarget(newIP.String())
   301  	}
   302  
   303  	return nil
   304  }
   305  
   306  func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
   307  	api := &CloudflareApi{}
   308  	api.ApiUser, api.ApiKey = m["apiuser"], m["apikey"]
   309  	// check api keys from creds json file
   310  	if api.ApiKey == "" || api.ApiUser == "" {
   311  		return nil, errors.Errorf("cloudflare apikey and apiuser must be provided")
   312  	}
   313  
   314  	// Check account data if set
   315  	api.AccountID, api.AccountName = m["accountid"], m["accountname"]
   316  	if (api.AccountID != "" && api.AccountName == "") || (api.AccountID == "" && api.AccountName != "") {
   317  		return nil, errors.Errorf("either both cloudflare accountid and accountname must be provided or neither")
   318  	}
   319  
   320  	err := api.fetchDomainList()
   321  	if err != nil {
   322  		return nil, err
   323  	}
   324  
   325  	if len(metadata) > 0 {
   326  		parsedMeta := &struct {
   327  			IPConversions   string   `json:"ip_conversions"`
   328  			IgnoredLabels   []string `json:"ignored_labels"`
   329  			ManageRedirects bool     `json:"manage_redirects"`
   330  		}{}
   331  		err := json.Unmarshal([]byte(metadata), parsedMeta)
   332  		if err != nil {
   333  			return nil, err
   334  		}
   335  		api.manageRedirects = parsedMeta.ManageRedirects
   336  		// ignored_labels:
   337  		for _, l := range parsedMeta.IgnoredLabels {
   338  			api.ignoredLabels = append(api.ignoredLabels, l)
   339  		}
   340  		if len(api.ignoredLabels) > 0 {
   341  			printer.Warnf("Cloudflare 'ignored_labels' configuration is deprecated and might be removed. Please use the IGNORE domain directive to achieve the same effect.\n")
   342  		}
   343  		// parse provider level metadata
   344  		if len(parsedMeta.IPConversions) > 0 {
   345  			api.ipConversions, err = transform.DecodeTransformTable(parsedMeta.IPConversions)
   346  			if err != nil {
   347  				return nil, err
   348  			}
   349  		}
   350  	}
   351  	return api, nil
   352  }
   353  
   354  // Used on the "existing" records.
   355  type cfRecData struct {
   356  	Name     string `json:"name"`
   357  	Target   string `json:"target"`
   358  	Service  string `json:"service"`  // SRV
   359  	Proto    string `json:"proto"`    // SRV
   360  	Priority uint16 `json:"priority"` // SRV
   361  	Weight   uint16 `json:"weight"`   // SRV
   362  	Port     uint16 `json:"port"`     // SRV
   363  	Tag      string `json:"tag"`      // CAA
   364  	Flags    uint8  `json:"flags"`    // CAA
   365  	Value    string `json:"value"`    // CAA
   366  }
   367  
   368  type cfRecord struct {
   369  	ID         string      `json:"id"`
   370  	Type       string      `json:"type"`
   371  	Name       string      `json:"name"`
   372  	Content    string      `json:"content"`
   373  	Proxiable  bool        `json:"proxiable"`
   374  	Proxied    bool        `json:"proxied"`
   375  	TTL        uint32      `json:"ttl"`
   376  	Locked     bool        `json:"locked"`
   377  	ZoneID     string      `json:"zone_id"`
   378  	ZoneName   string      `json:"zone_name"`
   379  	CreatedOn  time.Time   `json:"created_on"`
   380  	ModifiedOn time.Time   `json:"modified_on"`
   381  	Data       *cfRecData  `json:"data"`
   382  	Priority   json.Number `json:"priority"`
   383  }
   384  
   385  func (c *cfRecord) nativeToRecord(domain string) *models.RecordConfig {
   386  	// normalize cname,mx,ns records with dots to be consistent with our config format.
   387  	if c.Type == "CNAME" || c.Type == "MX" || c.Type == "NS" || c.Type == "SRV" {
   388  		c.Content = dnsutil.AddOrigin(c.Content+".", domain)
   389  	}
   390  
   391  	rc := &models.RecordConfig{
   392  		TTL:      c.TTL,
   393  		Original: c,
   394  	}
   395  	rc.SetLabelFromFQDN(c.Name, domain)
   396  	switch rType := c.Type; rType { // #rtype_variations
   397  	case "MX":
   398  		var priority uint16
   399  		if c.Priority == "" {
   400  			priority = 0
   401  		} else {
   402  			if p, err := c.Priority.Int64(); err != nil {
   403  				panic(errors.Wrap(err, "error decoding priority from cloudflare record"))
   404  			} else {
   405  				priority = uint16(p)
   406  			}
   407  		}
   408  		if err := rc.SetTargetMX(priority, c.Content); err != nil {
   409  			panic(errors.Wrap(err, "unparsable MX record received from cloudflare"))
   410  		}
   411  	case "SRV":
   412  		data := *c.Data
   413  		if err := rc.SetTargetSRV(data.Priority, data.Weight, data.Port,
   414  			dnsutil.AddOrigin(data.Target+".", domain)); err != nil {
   415  			panic(errors.Wrap(err, "unparsable SRV record received from cloudflare"))
   416  		}
   417  	default: // "A", "AAAA", "ANAME", "CAA", "CNAME", "NS", "PTR", "TXT"
   418  		if err := rc.PopulateFromString(rType, c.Content, domain); err != nil {
   419  			panic(errors.Wrap(err, "unparsable record received from cloudflare"))
   420  		}
   421  	}
   422  
   423  	return rc
   424  }
   425  
   426  func getProxyMetadata(r *models.RecordConfig) map[string]string {
   427  	if r.Type != "A" && r.Type != "AAAA" && r.Type != "CNAME" {
   428  		return nil
   429  	}
   430  	proxied := false
   431  	if r.Original != nil {
   432  		proxied = r.Original.(*cfRecord).Proxied
   433  	} else {
   434  		proxied = r.Metadata[metaProxy] != "off"
   435  	}
   436  	return map[string]string{
   437  		"proxy": fmt.Sprint(proxied),
   438  	}
   439  }
   440  
   441  // EnsureDomainExists returns an error of domain does not exist.
   442  func (c *CloudflareApi) EnsureDomainExists(domain string) error {
   443  	if _, ok := c.domainIndex[domain]; ok {
   444  		return nil
   445  	}
   446  	var id string
   447  	id, err := c.createZone(domain)
   448  	fmt.Printf("Added zone for %s to Cloudflare account: %s\n", domain, id)
   449  	return err
   450  }