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