github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/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/miekg/dns/dnsutil"
    12  
    13  	"github.com/StackExchange/dnscontrol/v2/models"
    14  	"github.com/StackExchange/dnscontrol/v2/pkg/printer"
    15  	"github.com/StackExchange/dnscontrol/v2/pkg/transform"
    16  	"github.com/StackExchange/dnscontrol/v2/providers"
    17  	"github.com/StackExchange/dnscontrol/v2/providers/diff"
    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.CanUsePTR:              providers.Cannot(),
    43  	providers.CanUseCAA:              providers.Can(),
    44  	providers.CanUseSRV:              providers.Can(),
    45  	providers.CanUseTLSA:             providers.Can(),
    46  	providers.CanUseSSHFP:            providers.Can(),
    47  	providers.DocCreateDomains:       providers.Can(),
    48  	providers.DocDualHost:            providers.Cannot("Cloudflare will not work well in situations where it is not the only DNS server"),
    49  	providers.DocOfficiallySupported: providers.Can(),
    50  	providers.CanGetZones:            providers.Can(),
    51  }
    52  
    53  func init() {
    54  	providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", newCloudflare, features)
    55  	providers.RegisterCustomRecordType("CF_REDIRECT", "CLOUDFLAREAPI", "")
    56  	providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", "CLOUDFLAREAPI", "")
    57  }
    58  
    59  // CloudflareApi is the handle for API calls.
    60  type CloudflareApi struct {
    61  	ApiKey          string `json:"apikey"`
    62  	ApiToken        string `json:"apitoken"`
    63  	ApiUser         string `json:"apiuser"`
    64  	AccountID       string `json:"accountid"`
    65  	AccountName     string `json:"accountname"`
    66  	domainIndex     map[string]string
    67  	nameservers     map[string][]string
    68  	ipConversions   []transform.IpConversion
    69  	ignoredLabels   []string
    70  	manageRedirects bool
    71  }
    72  
    73  func labelMatches(label string, matches []string) bool {
    74  	printer.Debugf("DEBUG: labelMatches(%#v, %#v)\n", label, matches)
    75  	for _, tst := range matches {
    76  		if label == tst {
    77  			return true
    78  		}
    79  	}
    80  	return false
    81  }
    82  
    83  // GetNameservers returns the nameservers for a domain.
    84  func (c *CloudflareApi) GetNameservers(domain string) ([]*models.Nameserver, error) {
    85  	if c.domainIndex == nil {
    86  		if err := c.fetchDomainList(); err != nil {
    87  			return nil, err
    88  		}
    89  	}
    90  	ns, ok := c.nameservers[domain]
    91  	if !ok {
    92  		return nil, fmt.Errorf("Nameservers for %s not found in cloudflare account", domain)
    93  	}
    94  	return models.StringsToNameservers(ns), nil
    95  }
    96  
    97  func (c *CloudflareApi) ListZones() ([]string, error) {
    98  	if err := c.fetchDomainList(); err != nil {
    99  		return nil, err
   100  	}
   101  	zones := make([]string, 0, len(c.domainIndex))
   102  	for d := range c.domainIndex {
   103  		zones = append(zones, d)
   104  	}
   105  	return zones, nil
   106  }
   107  
   108  // GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
   109  func (c *CloudflareApi) GetZoneRecords(domain string) (models.Records, error) {
   110  	id, err := c.getDomainID(domain)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  	records, err := c.getRecordsForDomain(id, domain)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  	for _, rec := range records {
   119  		if rec.TTL == 1 {
   120  			rec.TTL = 0
   121  		}
   122  	}
   123  	return records, nil
   124  }
   125  
   126  func (c *CloudflareApi) getDomainID(name string) (string, error) {
   127  	if c.domainIndex == nil {
   128  		if err := c.fetchDomainList(); err != nil {
   129  			return "", err
   130  		}
   131  	}
   132  	id, ok := c.domainIndex[name]
   133  	if !ok {
   134  		return "", fmt.Errorf("'%s' not a zone in cloudflare account", name)
   135  	}
   136  	return id, nil
   137  }
   138  
   139  // GetDomainCorrections returns a list of corrections to update a domain.
   140  func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
   141  	id, err := c.getDomainID(dc.Name)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	records, err := c.getRecordsForDomain(id, dc.Name)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	if err := c.preprocessConfig(dc); err != nil {
   151  		return nil, err
   152  	}
   153  	for i := len(records) - 1; i >= 0; i-- {
   154  		rec := records[i]
   155  		// Delete ignore labels
   156  		if labelMatches(dnsutil.TrimDomainName(rec.Original.(*cfRecord).Name, dc.Name), c.ignoredLabels) {
   157  			printer.Debugf("ignored_label: %s\n", rec.Original.(*cfRecord).Name)
   158  			records = append(records[:i], records[i+1:]...)
   159  		}
   160  	}
   161  
   162  	if c.manageRedirects {
   163  		prs, err := c.getPageRules(id, dc.Name)
   164  		if err != nil {
   165  			return nil, err
   166  		}
   167  		records = append(records, prs...)
   168  	}
   169  
   170  	for _, rec := range dc.Records {
   171  		if rec.Type == "ALIAS" {
   172  			rec.Type = "CNAME"
   173  		}
   174  		// As per CF-API documentation proxied records are always forced to have a TTL of 1.
   175  		// When not forcing this property change here, dnscontrol tries each time to update
   176  		// the TTL of a record which simply cannot be changed anyway.
   177  		if rec.Metadata[metaProxy] != "off" {
   178  			rec.TTL = 1
   179  		}
   180  		if labelMatches(rec.GetLabel(), c.ignoredLabels) {
   181  			log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.GetLabel(), c.ignoredLabels)
   182  		}
   183  	}
   184  
   185  	checkNSModifications(dc)
   186  
   187  	// Normalize
   188  	models.PostProcessRecords(records)
   189  
   190  	differ := diff.New(dc, getProxyMetadata)
   191  	_, create, del, mod := differ.IncrementalDiff(records)
   192  	corrections := []*models.Correction{}
   193  
   194  	for _, d := range del {
   195  		ex := d.Existing
   196  		if ex.Type == "PAGE_RULE" {
   197  			corrections = append(corrections, &models.Correction{
   198  				Msg: d.String(),
   199  				F:   func() error { return c.deletePageRule(ex.Original.(*pageRule).ID, id) },
   200  			})
   201  
   202  		} else {
   203  			corrections = append(corrections, c.deleteRec(ex.Original.(*cfRecord), id))
   204  		}
   205  	}
   206  	for _, d := range create {
   207  		des := d.Desired
   208  		if des.Type == "PAGE_RULE" {
   209  			corrections = append(corrections, &models.Correction{
   210  				Msg: d.String(),
   211  				F:   func() error { return c.createPageRule(id, des.GetTargetField()) },
   212  			})
   213  		} else {
   214  			corrections = append(corrections, c.createRec(des, id)...)
   215  		}
   216  	}
   217  
   218  	for _, d := range mod {
   219  		rec := d.Desired
   220  		ex := d.Existing
   221  		if rec.Type == "PAGE_RULE" {
   222  			corrections = append(corrections, &models.Correction{
   223  				Msg: d.String(),
   224  				F:   func() error { return c.updatePageRule(ex.Original.(*pageRule).ID, id, rec.GetTargetField()) },
   225  			})
   226  		} else {
   227  			e := ex.Original.(*cfRecord)
   228  			proxy := e.Proxiable && rec.Metadata[metaProxy] != "off"
   229  			corrections = append(corrections, &models.Correction{
   230  				Msg: d.String(),
   231  				F:   func() error { return c.modifyRecord(id, e.ID, proxy, rec) },
   232  			})
   233  		}
   234  	}
   235  
   236  	// Add universalSSL change to corrections when needed
   237  	if changed, newState, err := c.checkUniversalSSL(dc, id); err == nil && changed {
   238  		var newStateString string
   239  		if newState {
   240  			newStateString = "enabled"
   241  		} else {
   242  			newStateString = "disabled"
   243  		}
   244  		corrections = append(corrections, &models.Correction{
   245  			Msg: fmt.Sprintf("Universal SSL will be %s for this domain.", newStateString),
   246  			F:   func() error { return c.changeUniversalSSL(id, newState) },
   247  		})
   248  	}
   249  
   250  	return corrections, nil
   251  }
   252  
   253  func checkNSModifications(dc *models.DomainConfig) {
   254  	newList := make([]*models.RecordConfig, 0, len(dc.Records))
   255  	for _, rec := range dc.Records {
   256  		if rec.Type == "NS" && rec.GetLabelFQDN() == dc.Name {
   257  			if !strings.HasSuffix(rec.GetTargetField(), ".ns.cloudflare.com.") {
   258  				printer.Warnf("cloudflare does not support modifying NS records on base domain. %s will not be added.\n", rec.GetTargetField())
   259  			}
   260  			continue
   261  		}
   262  		newList = append(newList, rec)
   263  	}
   264  	dc.Records = newList
   265  }
   266  
   267  func (c *CloudflareApi) checkUniversalSSL(dc *models.DomainConfig, id string) (changed bool, newState bool, err error) {
   268  	expected_str := dc.Metadata[metaUniversalSSL]
   269  	if expected_str == "" {
   270  		return false, false, fmt.Errorf("Metadata not set.")
   271  	}
   272  
   273  	if actual, err := c.getUniversalSSL(id); err == nil {
   274  		// convert str to bool
   275  		var expected bool
   276  		if expected_str == "off" {
   277  			expected = false
   278  		} else {
   279  			expected = true
   280  		}
   281  		// did something change?
   282  		if actual != expected {
   283  			return true, expected, nil
   284  		}
   285  		return false, expected, nil
   286  	}
   287  	return false, false, fmt.Errorf("error receiving universal ssl state:")
   288  }
   289  
   290  const (
   291  	metaProxy         = "cloudflare_proxy"
   292  	metaProxyDefault  = metaProxy + "_default"
   293  	metaOriginalIP    = "original_ip" // TODO(tlim): Unclear what this means.
   294  	metaUniversalSSL  = "cloudflare_universalssl"
   295  	metaIPConversions = "ip_conversions" // TODO(tlim): Rename to obscure_rules.
   296  )
   297  
   298  func checkProxyVal(v string) (string, error) {
   299  	v = strings.ToLower(v)
   300  	if v != "on" && v != "off" && v != "full" {
   301  		return "", fmt.Errorf("Bad metadata value for cloudflare_proxy: '%s'. Use on/off/full", v)
   302  	}
   303  	return v, nil
   304  }
   305  
   306  func (c *CloudflareApi) preprocessConfig(dc *models.DomainConfig) error {
   307  
   308  	// Determine the default proxy setting.
   309  	var defProxy string
   310  	var err error
   311  	if defProxy = dc.Metadata[metaProxyDefault]; defProxy == "" {
   312  		defProxy = "off"
   313  	} else {
   314  		defProxy, err = checkProxyVal(defProxy)
   315  		if err != nil {
   316  			return err
   317  		}
   318  	}
   319  
   320  	// Check UniversalSSL setting
   321  	if u := dc.Metadata[metaUniversalSSL]; u != "" {
   322  		u = strings.ToLower(u)
   323  		if u != "on" && u != "off" {
   324  			return fmt.Errorf("Bad metadata value for %s: '%s'. Use on/off.", metaUniversalSSL, u)
   325  		}
   326  	}
   327  
   328  	// Normalize the proxy setting for each record.
   329  	// A and CNAMEs: Validate. If null, set to default.
   330  	// else: Make sure it wasn't set.  Set to default.
   331  	// iterate backwards so first defined page rules have highest priority
   332  	currentPrPrio := 1
   333  	for i := len(dc.Records) - 1; i >= 0; i-- {
   334  		rec := dc.Records[i]
   335  		if rec.Metadata == nil {
   336  			rec.Metadata = map[string]string{}
   337  		}
   338  		// cloudflare uses "1" to mean "auto-ttl"
   339  		// if we get here and ttl is not specified (or is the dnscontrol default of 300),
   340  		// use automatic mode instead.
   341  		if rec.TTL == 0 || rec.TTL == 300 {
   342  			rec.TTL = 1
   343  		}
   344  		if rec.TTL != 1 && rec.TTL < 120 {
   345  			rec.TTL = 120
   346  		}
   347  
   348  		if rec.Type != "A" && rec.Type != "CNAME" && rec.Type != "AAAA" && rec.Type != "ALIAS" {
   349  			if rec.Metadata[metaProxy] != "" {
   350  				return fmt.Errorf("cloudflare_proxy set on %v record: %#v cloudflare_proxy=%#v", rec.Type, rec.GetLabel(), rec.Metadata[metaProxy])
   351  			}
   352  			// Force it to off.
   353  			rec.Metadata[metaProxy] = "off"
   354  		} else {
   355  			if val := rec.Metadata[metaProxy]; val == "" {
   356  				rec.Metadata[metaProxy] = defProxy
   357  			} else {
   358  				val, err := checkProxyVal(val)
   359  				if err != nil {
   360  					return err
   361  				}
   362  				rec.Metadata[metaProxy] = val
   363  			}
   364  		}
   365  
   366  		// CF_REDIRECT record types. Encode target as $FROM,$TO,$PRIO,$CODE
   367  		if rec.Type == "CF_REDIRECT" || rec.Type == "CF_TEMP_REDIRECT" {
   368  			if !c.manageRedirects {
   369  				return fmt.Errorf("you must add 'manage_redirects: true' metadata to cloudflare provider to use CF_REDIRECT records")
   370  			}
   371  			parts := strings.Split(rec.GetTargetField(), ",")
   372  			if len(parts) != 2 {
   373  				return fmt.Errorf("Invalid data specified for cloudflare redirect record")
   374  			}
   375  			code := 301
   376  			if rec.Type == "CF_TEMP_REDIRECT" {
   377  				code = 302
   378  			}
   379  			rec.SetTarget(fmt.Sprintf("%s,%d,%d", rec.GetTargetField(), currentPrPrio, code))
   380  			currentPrPrio++
   381  			rec.Type = "PAGE_RULE"
   382  		}
   383  	}
   384  
   385  	// look for ip conversions and transform records
   386  	for _, rec := range dc.Records {
   387  		if rec.Type != "A" {
   388  			continue
   389  		}
   390  		// only transform "full"
   391  		if rec.Metadata[metaProxy] != "full" {
   392  			continue
   393  		}
   394  		ip := net.ParseIP(rec.GetTargetField())
   395  		if ip == nil {
   396  			return fmt.Errorf("%s is not a valid ip address", rec.GetTargetField())
   397  		}
   398  		newIP, err := transform.TransformIP(ip, c.ipConversions)
   399  		if err != nil {
   400  			return err
   401  		}
   402  		rec.Metadata[metaOriginalIP] = rec.GetTargetField()
   403  		rec.SetTarget(newIP.String())
   404  	}
   405  
   406  	return nil
   407  }
   408  
   409  func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
   410  	api := &CloudflareApi{}
   411  	api.ApiUser, api.ApiKey, api.ApiToken = m["apiuser"], m["apikey"], m["apitoken"]
   412  	// check api keys from creds json file
   413  	if api.ApiToken == "" && (api.ApiKey == "" || api.ApiUser == "") {
   414  		return nil, fmt.Errorf("if cloudflare apitoken is not set, apikey and apiuser must be provided")
   415  	}
   416  	if api.ApiToken != "" && (api.ApiKey != "" || api.ApiUser != "") {
   417  		return nil, fmt.Errorf("if cloudflare apitoken is set, apikey and apiuser should not be provided")
   418  	}
   419  
   420  	// Check account data if set
   421  	api.AccountID, api.AccountName = m["accountid"], m["accountname"]
   422  	if (api.AccountID != "" && api.AccountName == "") || (api.AccountID == "" && api.AccountName != "") {
   423  		return nil, fmt.Errorf("either both cloudflare accountid and accountname must be provided or neither")
   424  	}
   425  
   426  	err := api.fetchDomainList()
   427  	if err != nil {
   428  		return nil, err
   429  	}
   430  
   431  	if len(metadata) > 0 {
   432  		parsedMeta := &struct {
   433  			IPConversions   string   `json:"ip_conversions"`
   434  			IgnoredLabels   []string `json:"ignored_labels"`
   435  			ManageRedirects bool     `json:"manage_redirects"`
   436  		}{}
   437  		err := json.Unmarshal([]byte(metadata), parsedMeta)
   438  		if err != nil {
   439  			return nil, err
   440  		}
   441  		api.manageRedirects = parsedMeta.ManageRedirects
   442  		// ignored_labels:
   443  		for _, l := range parsedMeta.IgnoredLabels {
   444  			api.ignoredLabels = append(api.ignoredLabels, l)
   445  		}
   446  		if len(api.ignoredLabels) > 0 {
   447  			printer.Warnf("Cloudflare 'ignored_labels' configuration is deprecated and might be removed. Please use the IGNORE domain directive to achieve the same effect.\n")
   448  		}
   449  		// parse provider level metadata
   450  		if len(parsedMeta.IPConversions) > 0 {
   451  			api.ipConversions, err = transform.DecodeTransformTable(parsedMeta.IPConversions)
   452  			if err != nil {
   453  				return nil, err
   454  			}
   455  		}
   456  	}
   457  	return api, nil
   458  }
   459  
   460  // Used on the "existing" records.
   461  type cfRecData struct {
   462  	Name          string   `json:"name"`
   463  	Target        cfTarget `json:"target"`
   464  	Service       string   `json:"service"`       // SRV
   465  	Proto         string   `json:"proto"`         // SRV
   466  	Priority      uint16   `json:"priority"`      // SRV
   467  	Weight        uint16   `json:"weight"`        // SRV
   468  	Port          uint16   `json:"port"`          // SRV
   469  	Tag           string   `json:"tag"`           // CAA
   470  	Flags         uint8    `json:"flags"`         // CAA
   471  	Value         string   `json:"value"`         // CAA
   472  	Usage         uint8    `json:"usage"`         // TLSA
   473  	Selector      uint8    `json:"selector"`      // TLSA
   474  	Matching_Type uint8    `json:"matching_type"` // TLSA
   475  	Certificate   string   `json:"certificate"`   // TLSA
   476  	Algorithm     uint8    `json:"algorithm"`     // SSHFP
   477  	Hash_Type     uint8    `json:"type"`          // SSHFP
   478  	Fingerprint   string   `json:"fingerprint"`   // SSHFP
   479  }
   480  
   481  // cfTarget is a SRV target. A null target is represented by an empty string, but
   482  // a dot is so acceptable.
   483  type cfTarget string
   484  
   485  // UnmarshalJSON decodes a SRV target from the Cloudflare API. A null target is
   486  // represented by a false boolean or a dot. Domain names are FQDNs without a
   487  // trailing period (as of 2019-11-05).
   488  func (c *cfTarget) UnmarshalJSON(data []byte) error {
   489  	var obj interface{}
   490  	if err := json.Unmarshal(data, &obj); err != nil {
   491  		return err
   492  	}
   493  	switch v := obj.(type) {
   494  	case string:
   495  		*c = cfTarget(v)
   496  	case bool:
   497  		if v {
   498  			panic("unknown value for cfTarget bool: true")
   499  		}
   500  		*c = "" // the "." is already added by nativeToRecord
   501  	}
   502  	return nil
   503  }
   504  
   505  // MarshalJSON encodes cfTarget for the Cloudflare API. Null targets are
   506  // represented by a single period.
   507  func (c cfTarget) MarshalJSON() ([]byte, error) {
   508  	var obj string
   509  	switch c {
   510  	case "", ".":
   511  		obj = "."
   512  	default:
   513  		obj = string(c)
   514  	}
   515  	return json.Marshal(obj)
   516  }
   517  
   518  // DNSControlString returns cfTarget normalized to be a FQDN. Null targets are
   519  // represented by a single period.
   520  func (c cfTarget) FQDN() string {
   521  	return strings.TrimRight(string(c), ".") + "."
   522  }
   523  
   524  type cfRecord struct {
   525  	ID         string      `json:"id"`
   526  	Type       string      `json:"type"`
   527  	Name       string      `json:"name"`
   528  	Content    string      `json:"content"`
   529  	Proxiable  bool        `json:"proxiable"`
   530  	Proxied    bool        `json:"proxied"`
   531  	TTL        uint32      `json:"ttl"`
   532  	Locked     bool        `json:"locked"`
   533  	ZoneID     string      `json:"zone_id"`
   534  	ZoneName   string      `json:"zone_name"`
   535  	CreatedOn  time.Time   `json:"created_on"`
   536  	ModifiedOn time.Time   `json:"modified_on"`
   537  	Data       *cfRecData  `json:"data"`
   538  	Priority   json.Number `json:"priority"`
   539  }
   540  
   541  func (c *cfRecord) nativeToRecord(domain string) *models.RecordConfig {
   542  	// normalize cname,mx,ns records with dots to be consistent with our config format.
   543  	if c.Type == "CNAME" || c.Type == "MX" || c.Type == "NS" || c.Type == "SRV" {
   544  		c.Content = dnsutil.AddOrigin(c.Content+".", domain)
   545  	}
   546  
   547  	rc := &models.RecordConfig{
   548  		TTL:      c.TTL,
   549  		Original: c,
   550  	}
   551  	rc.SetLabelFromFQDN(c.Name, domain)
   552  
   553  	// workaround for https://github.com/StackExchange/dnscontrol/issues/446
   554  	if c.Type == "SPF" {
   555  		c.Type = "TXT"
   556  	}
   557  
   558  	switch rType := c.Type; rType { // #rtype_variations
   559  	case "MX":
   560  		var priority uint16
   561  		if c.Priority == "" {
   562  			priority = 0
   563  		} else {
   564  			if p, err := c.Priority.Int64(); err != nil {
   565  				panic(fmt.Errorf("error decoding priority from cloudflare record: %w", err))
   566  			} else {
   567  				priority = uint16(p)
   568  			}
   569  		}
   570  		if err := rc.SetTargetMX(priority, c.Content); err != nil {
   571  			panic(fmt.Errorf("unparsable MX record received from cloudflare: %w", err))
   572  		}
   573  	case "SRV":
   574  		data := *c.Data
   575  		if err := rc.SetTargetSRV(data.Priority, data.Weight, data.Port,
   576  			dnsutil.AddOrigin(data.Target.FQDN(), domain)); err != nil {
   577  			panic(fmt.Errorf("unparsable SRV record received from cloudflare: %w", err))
   578  		}
   579  	default: // "A", "AAAA", "ANAME", "CAA", "CNAME", "NS", "PTR", "TXT"
   580  		if err := rc.PopulateFromString(rType, c.Content, domain); err != nil {
   581  			panic(fmt.Errorf("unparsable record received from cloudflare: %w", err))
   582  		}
   583  	}
   584  
   585  	return rc
   586  }
   587  
   588  func getProxyMetadata(r *models.RecordConfig) map[string]string {
   589  	if r.Type != "A" && r.Type != "AAAA" && r.Type != "CNAME" {
   590  		return nil
   591  	}
   592  	proxied := false
   593  	if r.Original != nil {
   594  		proxied = r.Original.(*cfRecord).Proxied
   595  	} else {
   596  		proxied = r.Metadata[metaProxy] != "off"
   597  	}
   598  	return map[string]string{
   599  		"proxy": fmt.Sprint(proxied),
   600  	}
   601  }
   602  
   603  // EnsureDomainExists returns an error of domain does not exist.
   604  func (c *CloudflareApi) EnsureDomainExists(domain string) error {
   605  	if _, ok := c.domainIndex[domain]; ok {
   606  		return nil
   607  	}
   608  	var id string
   609  	id, err := c.createZone(domain)
   610  	fmt.Printf("Added zone for %s to Cloudflare account: %s\n", domain, id)
   611  	return err
   612  }