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