github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/providers/cloudflare/rest.go (about)

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