github.com/StackExchange/DNSControl@v0.2.8/providers/cloudflare/rest.go (about)

     1  package cloudflare
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/StackExchange/dnscontrol/models"
    13  	"github.com/pkg/errors"
    14  )
    15  
    16  const (
    17  	baseURL           = "https://api.cloudflare.com/client/v4/"
    18  	zonesURL          = baseURL + "zones/"
    19  	recordsURL        = zonesURL + "%s/dns_records/"
    20  	pageRulesURL      = zonesURL + "%s/pagerules/"
    21  	singlePageRuleURL = pageRulesURL + "%s"
    22  	singleRecordURL   = recordsURL + "%s"
    23  )
    24  
    25  // get list of domains for account. Cache so the ids can be looked up from domain name
    26  func (c *CloudflareApi) fetchDomainList() error {
    27  	c.domainIndex = map[string]string{}
    28  	c.nameservers = map[string][]string{}
    29  	page := 1
    30  	for {
    31  		zr := &zoneResponse{}
    32  		url := fmt.Sprintf("%s?page=%d&per_page=50", zonesURL, page)
    33  		if err := c.get(url, zr); err != nil {
    34  			return errors.Errorf("Error fetching domain list from cloudflare: %s", err)
    35  		}
    36  		if !zr.Success {
    37  			return errors.Errorf("Error fetching domain list from cloudflare: %s", stringifyErrors(zr.Errors))
    38  		}
    39  		for _, zone := range zr.Result {
    40  			c.domainIndex[zone.Name] = zone.ID
    41  			for _, ns := range zone.Nameservers {
    42  				c.nameservers[zone.Name] = append(c.nameservers[zone.Name], ns)
    43  			}
    44  		}
    45  		ri := zr.ResultInfo
    46  		if len(zr.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount {
    47  			break
    48  		}
    49  		page++
    50  	}
    51  	return nil
    52  }
    53  
    54  // get all records for a domain
    55  func (c *CloudflareApi) getRecordsForDomain(id string, domain string) ([]*models.RecordConfig, error) {
    56  	url := fmt.Sprintf(recordsURL, id)
    57  	page := 1
    58  	records := []*models.RecordConfig{}
    59  	for {
    60  		reqURL := fmt.Sprintf("%s?page=%d&per_page=100", url, page)
    61  		var data recordsResponse
    62  		if err := c.get(reqURL, &data); err != nil {
    63  			return nil, errors.Errorf("Error fetching record list from cloudflare: %s", err)
    64  		}
    65  		if !data.Success {
    66  			return nil, errors.Errorf("Error fetching record list cloudflare: %s", stringifyErrors(data.Errors))
    67  		}
    68  		for _, rec := range data.Result {
    69  			// fmt.Printf("REC: %+v\n", rec)
    70  			records = append(records, rec.nativeToRecord(domain))
    71  		}
    72  		ri := data.ResultInfo
    73  		if len(data.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount {
    74  			break
    75  		}
    76  		page++
    77  	}
    78  	// fmt.Printf("DEBUG REORDS=%v\n", records)
    79  	return records, nil
    80  }
    81  
    82  // create a correction to delete a record
    83  func (c *CloudflareApi) deleteRec(rec *cfRecord, domainID string) *models.Correction {
    84  	return &models.Correction{
    85  		Msg: fmt.Sprintf("DELETE record: %s %s %d %s (id=%s)", rec.Name, rec.Type, rec.TTL, rec.Content, rec.ID),
    86  		F: func() error {
    87  			endpoint := fmt.Sprintf(singleRecordURL, domainID, rec.ID)
    88  			req, err := http.NewRequest("DELETE", endpoint, nil)
    89  			if err != nil {
    90  				return err
    91  			}
    92  			c.setHeaders(req)
    93  			_, err = handleActionResponse(http.DefaultClient.Do(req))
    94  			return err
    95  		},
    96  	}
    97  }
    98  
    99  func (c *CloudflareApi) createZone(domainName string) (string, error) {
   100  	type createZone struct {
   101  		Name string `json:"name"`
   102  
   103  		Account struct {
   104  			ID   string `json:"id"`
   105  			Name string `json:"name"`
   106  		} `json:"account"`
   107  	}
   108  	var id string
   109  	cz := &createZone{
   110  		Name: domainName}
   111  
   112  	if c.AccountID != "" || c.AccountName != "" {
   113  		cz.Account.ID = c.AccountID
   114  		cz.Account.Name = c.AccountName
   115  	}
   116  
   117  	buf := &bytes.Buffer{}
   118  	encoder := json.NewEncoder(buf)
   119  	if err := encoder.Encode(cz); err != nil {
   120  		return "", err
   121  	}
   122  	req, err := http.NewRequest("POST", zonesURL, buf)
   123  	if err != nil {
   124  		return "", err
   125  	}
   126  	c.setHeaders(req)
   127  	id, err = handleActionResponse(http.DefaultClient.Do(req))
   128  	return id, err
   129  }
   130  
   131  func cfSrvData(rec *models.RecordConfig) *cfRecData {
   132  	serverParts := strings.Split(rec.GetLabelFQDN(), ".")
   133  	return &cfRecData{
   134  		Service:  serverParts[0],
   135  		Proto:    serverParts[1],
   136  		Name:     strings.Join(serverParts[2:], "."),
   137  		Port:     rec.SrvPort,
   138  		Priority: rec.SrvPriority,
   139  		Weight:   rec.SrvWeight,
   140  		Target:   rec.GetTargetField(),
   141  	}
   142  }
   143  
   144  func cfCaaData(rec *models.RecordConfig) *cfRecData {
   145  	return &cfRecData{
   146  		Tag:   rec.CaaTag,
   147  		Flags: rec.CaaFlag,
   148  		Value: rec.GetTargetField(),
   149  	}
   150  }
   151  
   152  func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*models.Correction {
   153  	type createRecord struct {
   154  		Name     string     `json:"name"`
   155  		Type     string     `json:"type"`
   156  		Content  string     `json:"content"`
   157  		TTL      uint32     `json:"ttl"`
   158  		Priority uint16     `json:"priority"`
   159  		Data     *cfRecData `json:"data"`
   160  	}
   161  	var id string
   162  	content := rec.GetTargetField()
   163  	if rec.Metadata[metaOriginalIP] != "" {
   164  		content = rec.Metadata[metaOriginalIP]
   165  	}
   166  	prio := ""
   167  	if rec.Type == "MX" {
   168  		prio = fmt.Sprintf(" %d ", rec.MxPreference)
   169  	}
   170  	arr := []*models.Correction{{
   171  		Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.GetLabel(), rec.Type, rec.TTL, prio, content),
   172  		F: func() error {
   173  
   174  			cf := &createRecord{
   175  				Name:     rec.GetLabel(),
   176  				Type:     rec.Type,
   177  				TTL:      rec.TTL,
   178  				Content:  content,
   179  				Priority: rec.MxPreference,
   180  			}
   181  			if rec.Type == "SRV" {
   182  				cf.Data = cfSrvData(rec)
   183  				cf.Name = rec.GetLabelFQDN()
   184  			} else if rec.Type == "CAA" {
   185  				cf.Data = cfCaaData(rec)
   186  				cf.Name = rec.GetLabelFQDN()
   187  				cf.Content = ""
   188  			}
   189  			endpoint := fmt.Sprintf(recordsURL, domainID)
   190  			buf := &bytes.Buffer{}
   191  			encoder := json.NewEncoder(buf)
   192  			if err := encoder.Encode(cf); err != nil {
   193  				return err
   194  			}
   195  			req, err := http.NewRequest("POST", endpoint, buf)
   196  			if err != nil {
   197  				return err
   198  			}
   199  			c.setHeaders(req)
   200  			id, err = handleActionResponse(http.DefaultClient.Do(req))
   201  			return err
   202  		},
   203  	}}
   204  	if rec.Metadata[metaProxy] != "off" {
   205  		arr = append(arr, &models.Correction{
   206  			Msg: fmt.Sprintf("ACTIVATE PROXY for new record %s %s %d %s", rec.GetLabel(), rec.Type, rec.TTL, rec.GetTargetField()),
   207  			F:   func() error { return c.modifyRecord(domainID, id, true, rec) },
   208  		})
   209  	}
   210  	return arr
   211  }
   212  
   213  func (c *CloudflareApi) modifyRecord(domainID, recID string, proxied bool, rec *models.RecordConfig) error {
   214  	if domainID == "" || recID == "" {
   215  		return errors.Errorf("cannot modify record if domain or record id are empty")
   216  	}
   217  	type record struct {
   218  		ID       string     `json:"id"`
   219  		Proxied  bool       `json:"proxied"`
   220  		Name     string     `json:"name"`
   221  		Type     string     `json:"type"`
   222  		Content  string     `json:"content"`
   223  		Priority uint16     `json:"priority"`
   224  		TTL      uint32     `json:"ttl"`
   225  		Data     *cfRecData `json:"data"`
   226  	}
   227  	r := record{
   228  		ID:       recID,
   229  		Proxied:  proxied,
   230  		Name:     rec.GetLabel(),
   231  		Type:     rec.Type,
   232  		Content:  rec.GetTargetField(),
   233  		Priority: rec.MxPreference,
   234  		TTL:      rec.TTL,
   235  		Data:     nil,
   236  	}
   237  	if rec.Type == "SRV" {
   238  		r.Data = cfSrvData(rec)
   239  		r.Name = rec.GetLabelFQDN()
   240  	} else if rec.Type == "CAA" {
   241  		r.Data = cfCaaData(rec)
   242  		r.Name = rec.GetLabelFQDN()
   243  		r.Content = ""
   244  	}
   245  	endpoint := fmt.Sprintf(singleRecordURL, domainID, recID)
   246  	buf := &bytes.Buffer{}
   247  	encoder := json.NewEncoder(buf)
   248  	if err := encoder.Encode(r); err != nil {
   249  		return err
   250  	}
   251  	req, err := http.NewRequest("PUT", endpoint, buf)
   252  	if err != nil {
   253  		return err
   254  	}
   255  	c.setHeaders(req)
   256  	_, err = handleActionResponse(http.DefaultClient.Do(req))
   257  	return err
   258  }
   259  
   260  // common error handling for all action responses
   261  func handleActionResponse(resp *http.Response, err error) (id string, e error) {
   262  	if err != nil {
   263  		return "", err
   264  	}
   265  	defer resp.Body.Close()
   266  	result := &basicResponse{}
   267  	decoder := json.NewDecoder(resp.Body)
   268  	if err = decoder.Decode(result); err != nil {
   269  		return "", errors.Errorf("Unknown error. Status code: %d", resp.StatusCode)
   270  	}
   271  	if resp.StatusCode != 200 {
   272  		return "", errors.Errorf(stringifyErrors(result.Errors))
   273  	}
   274  	return result.Result.ID, nil
   275  }
   276  
   277  func (c *CloudflareApi) setHeaders(req *http.Request) {
   278  	req.Header.Set("X-Auth-Key", c.ApiKey)
   279  	req.Header.Set("X-Auth-Email", c.ApiUser)
   280  }
   281  
   282  // generic get handler. makes request and unmarshalls response to given interface
   283  func (c *CloudflareApi) get(endpoint string, target interface{}) error {
   284  	req, err := http.NewRequest("GET", endpoint, nil)
   285  	if err != nil {
   286  		return err
   287  	}
   288  	c.setHeaders(req)
   289  	resp, err := http.DefaultClient.Do(req)
   290  	if err != nil {
   291  		return err
   292  	}
   293  	defer resp.Body.Close()
   294  	if resp.StatusCode != 200 {
   295  		return errors.Errorf("bad status code from cloudflare: %d not 200", resp.StatusCode)
   296  	}
   297  	decoder := json.NewDecoder(resp.Body)
   298  	return decoder.Decode(target)
   299  }
   300  
   301  func (c *CloudflareApi) getPageRules(id string, domain string) ([]*models.RecordConfig, error) {
   302  	url := fmt.Sprintf(pageRulesURL, id)
   303  	data := pageRuleResponse{}
   304  	if err := c.get(url, &data); err != nil {
   305  		return nil, errors.Errorf("Error fetching page rule list from cloudflare: %s", err)
   306  	}
   307  	if !data.Success {
   308  		return nil, errors.Errorf("Error fetching page rule list cloudflare: %s", stringifyErrors(data.Errors))
   309  	}
   310  	recs := []*models.RecordConfig{}
   311  	for _, pr := range data.Result {
   312  		// only interested in forwarding rules. Lets be very specific, and skip anything else
   313  		if len(pr.Actions) != 1 || len(pr.Targets) != 1 {
   314  			continue
   315  		}
   316  		if pr.Actions[0].ID != "forwarding_url" {
   317  			continue
   318  		}
   319  		err := json.Unmarshal([]byte(pr.Actions[0].Value), &pr.ForwardingInfo)
   320  		if err != nil {
   321  			return nil, err
   322  		}
   323  		var thisPr = pr
   324  		r := &models.RecordConfig{
   325  			Type:     "PAGE_RULE",
   326  			Original: thisPr,
   327  			TTL:      1,
   328  		}
   329  		r.SetLabel("@", domain)
   330  		r.SetTarget(fmt.Sprintf("%s,%s,%d,%d", // $FROM,$TO,$PRIO,$CODE
   331  			pr.Targets[0].Constraint.Value,
   332  			pr.ForwardingInfo.URL,
   333  			pr.Priority,
   334  			pr.ForwardingInfo.StatusCode))
   335  		recs = append(recs, r)
   336  	}
   337  	return recs, nil
   338  }
   339  
   340  func (c *CloudflareApi) deletePageRule(recordID, domainID string) error {
   341  	endpoint := fmt.Sprintf(singlePageRuleURL, domainID, recordID)
   342  	req, err := http.NewRequest("DELETE", endpoint, nil)
   343  	if err != nil {
   344  		return err
   345  	}
   346  	c.setHeaders(req)
   347  	_, err = handleActionResponse(http.DefaultClient.Do(req))
   348  	return err
   349  }
   350  
   351  func (c *CloudflareApi) updatePageRule(recordID, domainID string, target string) error {
   352  	if err := c.deletePageRule(recordID, domainID); err != nil {
   353  		return err
   354  	}
   355  	return c.createPageRule(domainID, target)
   356  }
   357  
   358  func (c *CloudflareApi) createPageRule(domainID string, target string) error {
   359  	endpoint := fmt.Sprintf(pageRulesURL, domainID)
   360  	return c.sendPageRule(endpoint, "POST", target)
   361  }
   362  
   363  func (c *CloudflareApi) sendPageRule(endpoint, method string, data string) error {
   364  	// from to priority code
   365  	parts := strings.Split(data, ",")
   366  	priority, _ := strconv.Atoi(parts[2])
   367  	code, _ := strconv.Atoi(parts[3])
   368  	fwdInfo := &pageRuleFwdInfo{
   369  		StatusCode: code,
   370  		URL:        parts[1],
   371  	}
   372  	dat, _ := json.Marshal(fwdInfo)
   373  	pr := &pageRule{
   374  		Status:   "active",
   375  		Priority: priority,
   376  		Targets: []pageRuleTarget{
   377  			{Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: parts[0]}},
   378  		},
   379  		Actions: []pageRuleAction{
   380  			{ID: "forwarding_url", Value: json.RawMessage(dat)},
   381  		},
   382  	}
   383  	buf := &bytes.Buffer{}
   384  	enc := json.NewEncoder(buf)
   385  	if err := enc.Encode(pr); err != nil {
   386  		return err
   387  	}
   388  	req, err := http.NewRequest(method, endpoint, buf)
   389  	if err != nil {
   390  		return err
   391  	}
   392  	c.setHeaders(req)
   393  	_, err = handleActionResponse(http.DefaultClient.Do(req))
   394  	return err
   395  }
   396  
   397  func stringifyErrors(errors []interface{}) string {
   398  	dat, err := json.Marshal(errors)
   399  	if err != nil {
   400  		return "???"
   401  	}
   402  	return string(dat)
   403  }
   404  
   405  type recordsResponse struct {
   406  	basicResponse
   407  	Result     []*cfRecord `json:"result"`
   408  	ResultInfo pagingInfo  `json:"result_info"`
   409  }
   410  
   411  type basicResponse struct {
   412  	Success  bool          `json:"success"`
   413  	Errors   []interface{} `json:"errors"`
   414  	Messages []interface{} `json:"messages"`
   415  	Result   struct {
   416  		ID string `json:"id"`
   417  	} `json:"result"`
   418  }
   419  
   420  type pageRuleResponse struct {
   421  	basicResponse
   422  	Result     []*pageRule `json:"result"`
   423  	ResultInfo pagingInfo  `json:"result_info"`
   424  }
   425  
   426  type pageRule struct {
   427  	ID             string           `json:"id,omitempty"`
   428  	Targets        []pageRuleTarget `json:"targets"`
   429  	Actions        []pageRuleAction `json:"actions"`
   430  	Priority       int              `json:"priority"`
   431  	Status         string           `json:"status"`
   432  	ModifiedOn     time.Time        `json:"modified_on,omitempty"`
   433  	CreatedOn      time.Time        `json:"created_on,omitempty"`
   434  	ForwardingInfo *pageRuleFwdInfo `json:"-"`
   435  }
   436  
   437  type pageRuleTarget struct {
   438  	Target     string             `json:"target"`
   439  	Constraint pageRuleConstraint `json:"constraint"`
   440  }
   441  
   442  type pageRuleConstraint struct {
   443  	Operator string `json:"operator"`
   444  	Value    string `json:"value"`
   445  }
   446  
   447  type pageRuleAction struct {
   448  	ID    string          `json:"id"`
   449  	Value json.RawMessage `json:"value"`
   450  }
   451  
   452  type pageRuleFwdInfo struct {
   453  	URL        string `json:"url"`
   454  	StatusCode int    `json:"status_code"`
   455  }
   456  
   457  type zoneResponse struct {
   458  	basicResponse
   459  	Result []struct {
   460  		ID          string   `json:"id"`
   461  		Name        string   `json:"name"`
   462  		Nameservers []string `json:"name_servers"`
   463  	} `json:"result"`
   464  	ResultInfo pagingInfo `json:"result_info"`
   465  }
   466  
   467  type pagingInfo struct {
   468  	Page       int `json:"page"`
   469  	PerPage    int `json:"per_page"`
   470  	Count      int `json:"count"`
   471  	TotalCount int `json:"total_count"`
   472  }