github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/cloudflare/rest.go (about)

     1  package cloudflare
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/StackExchange/dnscontrol/v2/models"
    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 fmt.Errorf("Error fetching domain list from cloudflare: %s", err)
    35  		}
    36  		if !zr.Success {
    37  			return fmt.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, fmt.Errorf("Error fetching record list from cloudflare: %s", err)
    64  		}
    65  		if !data.Success {
    66  			return nil, fmt.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  	c := &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  	}
   141  	c.Target = cfTarget(rec.GetTargetField())
   142  	return c
   143  }
   144  
   145  func cfCaaData(rec *models.RecordConfig) *cfRecData {
   146  	return &cfRecData{
   147  		Tag:   rec.CaaTag,
   148  		Flags: rec.CaaFlag,
   149  		Value: rec.GetTargetField(),
   150  	}
   151  }
   152  
   153  func cfTlsaData(rec *models.RecordConfig) *cfRecData {
   154  	return &cfRecData{
   155  		Usage:         rec.TlsaUsage,
   156  		Selector:      rec.TlsaSelector,
   157  		Matching_Type: rec.TlsaMatchingType,
   158  		Certificate:   rec.GetTargetField(),
   159  	}
   160  }
   161  
   162  func cfSshfpData(rec *models.RecordConfig) *cfRecData {
   163  	return &cfRecData{
   164  		Algorithm:   rec.SshfpAlgorithm,
   165  		Hash_Type:   rec.SshfpFingerprint,
   166  		Fingerprint: rec.GetTargetField(),
   167  	}
   168  }
   169  
   170  func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*models.Correction {
   171  	type createRecord struct {
   172  		Name     string     `json:"name"`
   173  		Type     string     `json:"type"`
   174  		Content  string     `json:"content"`
   175  		TTL      uint32     `json:"ttl"`
   176  		Priority uint16     `json:"priority"`
   177  		Data     *cfRecData `json:"data"`
   178  	}
   179  	var id string
   180  	content := rec.GetTargetField()
   181  	if rec.Metadata[metaOriginalIP] != "" {
   182  		content = rec.Metadata[metaOriginalIP]
   183  	}
   184  	prio := ""
   185  	if rec.Type == "MX" {
   186  		prio = fmt.Sprintf(" %d ", rec.MxPreference)
   187  	}
   188  	arr := []*models.Correction{{
   189  		Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.GetLabel(), rec.Type, rec.TTL, prio, content),
   190  		F: func() error {
   191  
   192  			cf := &createRecord{
   193  				Name:     rec.GetLabel(),
   194  				Type:     rec.Type,
   195  				TTL:      rec.TTL,
   196  				Content:  content,
   197  				Priority: rec.MxPreference,
   198  			}
   199  			if rec.Type == "SRV" {
   200  				cf.Data = cfSrvData(rec)
   201  				cf.Name = rec.GetLabelFQDN()
   202  			} else if rec.Type == "CAA" {
   203  				cf.Data = cfCaaData(rec)
   204  				cf.Name = rec.GetLabelFQDN()
   205  				cf.Content = ""
   206  			} else if rec.Type == "TLSA" {
   207  				cf.Data = cfTlsaData(rec)
   208  				cf.Name = rec.GetLabelFQDN()
   209  			} else if rec.Type == "SSHFP" {
   210  				cf.Data = cfSshfpData(rec)
   211  				cf.Name = rec.GetLabelFQDN()
   212  			}
   213  			endpoint := fmt.Sprintf(recordsURL, domainID)
   214  			buf := &bytes.Buffer{}
   215  			encoder := json.NewEncoder(buf)
   216  			if err := encoder.Encode(cf); err != nil {
   217  				return err
   218  			}
   219  			req, err := http.NewRequest("POST", endpoint, buf)
   220  			if err != nil {
   221  				return err
   222  			}
   223  			c.setHeaders(req)
   224  			id, err = handleActionResponse(http.DefaultClient.Do(req))
   225  			return err
   226  		},
   227  	}}
   228  	if rec.Metadata[metaProxy] != "off" {
   229  		arr = append(arr, &models.Correction{
   230  			Msg: fmt.Sprintf("ACTIVATE PROXY for new record %s %s %d %s", rec.GetLabel(), rec.Type, rec.TTL, rec.GetTargetField()),
   231  			F:   func() error { return c.modifyRecord(domainID, id, true, rec) },
   232  		})
   233  	}
   234  	return arr
   235  }
   236  
   237  func (c *CloudflareApi) modifyRecord(domainID, recID string, proxied bool, rec *models.RecordConfig) error {
   238  	if domainID == "" || recID == "" {
   239  		return fmt.Errorf("cannot modify record if domain or record id are empty")
   240  	}
   241  	type record struct {
   242  		ID       string     `json:"id"`
   243  		Proxied  bool       `json:"proxied"`
   244  		Name     string     `json:"name"`
   245  		Type     string     `json:"type"`
   246  		Content  string     `json:"content"`
   247  		Priority uint16     `json:"priority"`
   248  		TTL      uint32     `json:"ttl"`
   249  		Data     *cfRecData `json:"data"`
   250  	}
   251  	r := record{
   252  		ID:       recID,
   253  		Proxied:  proxied,
   254  		Name:     rec.GetLabel(),
   255  		Type:     rec.Type,
   256  		Content:  rec.GetTargetField(),
   257  		Priority: rec.MxPreference,
   258  		TTL:      rec.TTL,
   259  		Data:     nil,
   260  	}
   261  	if rec.Type == "SRV" {
   262  		r.Data = cfSrvData(rec)
   263  		r.Name = rec.GetLabelFQDN()
   264  	} else if rec.Type == "CAA" {
   265  		r.Data = cfCaaData(rec)
   266  		r.Name = rec.GetLabelFQDN()
   267  		r.Content = ""
   268  	} else if rec.Type == "TLSA" {
   269  		r.Data = cfTlsaData(rec)
   270  		r.Name = rec.GetLabelFQDN()
   271  	} else if rec.Type == "SSHFP" {
   272  		r.Data = cfSshfpData(rec)
   273  		r.Name = rec.GetLabelFQDN()
   274  	}
   275  	endpoint := fmt.Sprintf(singleRecordURL, domainID, recID)
   276  	buf := &bytes.Buffer{}
   277  	encoder := json.NewEncoder(buf)
   278  	if err := encoder.Encode(r); err != nil {
   279  		return err
   280  	}
   281  	req, err := http.NewRequest("PUT", endpoint, buf)
   282  	if err != nil {
   283  		return err
   284  	}
   285  	c.setHeaders(req)
   286  	_, err = handleActionResponse(http.DefaultClient.Do(req))
   287  	return err
   288  }
   289  
   290  // change universal ssl state
   291  func (c *CloudflareApi) changeUniversalSSL(domainID string, state bool) error {
   292  	type setUniversalSSL struct {
   293  		Enabled bool `json:"enabled"`
   294  	}
   295  	us := &setUniversalSSL{
   296  		Enabled: state,
   297  	}
   298  
   299  	// create json
   300  	buf := &bytes.Buffer{}
   301  	encoder := json.NewEncoder(buf)
   302  	if err := encoder.Encode(us); err != nil {
   303  		return err
   304  	}
   305  
   306  	// send request.
   307  	endpoint := fmt.Sprintf(zonesURL+"%s/ssl/universal/settings", domainID)
   308  	req, err := http.NewRequest("PATCH", endpoint, buf)
   309  	if err != nil {
   310  		return err
   311  	}
   312  	c.setHeaders(req)
   313  	_, err = handleActionResponse(http.DefaultClient.Do(req))
   314  
   315  	return err
   316  }
   317  
   318  // change universal ssl state
   319  func (c *CloudflareApi) getUniversalSSL(domainID string) (bool, error) {
   320  	type universalSSLResponse struct {
   321  		Success  bool          `json:"success"`
   322  		Errors   []interface{} `json:"errors"`
   323  		Messages []interface{} `json:"messages"`
   324  		Result   struct {
   325  			Enabled bool `json:"enabled"`
   326  		} `json:"result"`
   327  	}
   328  
   329  	// send request.
   330  	endpoint := fmt.Sprintf(zonesURL+"%s/ssl/universal/settings", domainID)
   331  	var result universalSSLResponse
   332  	err := c.get(endpoint, &result)
   333  	if err != nil {
   334  		return true, err
   335  	}
   336  
   337  	return result.Result.Enabled, err
   338  }
   339  
   340  // common error handling for all action responses
   341  func handleActionResponse(resp *http.Response, err error) (id string, e error) {
   342  	if err != nil {
   343  		return "", err
   344  	}
   345  	defer resp.Body.Close()
   346  	result := &basicResponse{}
   347  	decoder := json.NewDecoder(resp.Body)
   348  	if err = decoder.Decode(result); err != nil {
   349  		return "", fmt.Errorf("Unknown error. Status code: %d", resp.StatusCode)
   350  	}
   351  	if resp.StatusCode != 200 {
   352  		return "", fmt.Errorf(stringifyErrors(result.Errors))
   353  	}
   354  	return result.Result.ID, nil
   355  }
   356  
   357  func (c *CloudflareApi) setHeaders(req *http.Request) {
   358  	if len(c.ApiToken) > 0 {
   359  		req.Header.Set("Authorization", "Bearer "+c.ApiToken)
   360  	} else {
   361  		req.Header.Set("X-Auth-Key", c.ApiKey)
   362  		req.Header.Set("X-Auth-Email", c.ApiUser)
   363  	}
   364  }
   365  
   366  // generic get handler. makes request and unmarshalls response to given interface
   367  func (c *CloudflareApi) get(endpoint string, target interface{}) error {
   368  	req, err := http.NewRequest("GET", endpoint, nil)
   369  	if err != nil {
   370  		return err
   371  	}
   372  	c.setHeaders(req)
   373  	resp, err := http.DefaultClient.Do(req)
   374  	if err != nil {
   375  		return err
   376  	}
   377  	defer resp.Body.Close()
   378  	if resp.StatusCode != 200 {
   379  		dat, _ := ioutil.ReadAll(resp.Body)
   380  		fmt.Println(string(dat))
   381  		return fmt.Errorf("bad status code from cloudflare: %d not 200", resp.StatusCode)
   382  	}
   383  	decoder := json.NewDecoder(resp.Body)
   384  	return decoder.Decode(target)
   385  }
   386  
   387  func (c *CloudflareApi) getPageRules(id string, domain string) ([]*models.RecordConfig, error) {
   388  	url := fmt.Sprintf(pageRulesURL, id)
   389  	data := pageRuleResponse{}
   390  	if err := c.get(url, &data); err != nil {
   391  		return nil, fmt.Errorf("Error fetching page rule list from cloudflare: %s", err)
   392  	}
   393  	if !data.Success {
   394  		return nil, fmt.Errorf("Error fetching page rule list cloudflare: %s", stringifyErrors(data.Errors))
   395  	}
   396  	recs := []*models.RecordConfig{}
   397  	for _, pr := range data.Result {
   398  		// only interested in forwarding rules. Lets be very specific, and skip anything else
   399  		if len(pr.Actions) != 1 || len(pr.Targets) != 1 {
   400  			continue
   401  		}
   402  		if pr.Actions[0].ID != "forwarding_url" {
   403  			continue
   404  		}
   405  		err := json.Unmarshal([]byte(pr.Actions[0].Value), &pr.ForwardingInfo)
   406  		if err != nil {
   407  			return nil, err
   408  		}
   409  		var thisPr = pr
   410  		r := &models.RecordConfig{
   411  			Type:     "PAGE_RULE",
   412  			Original: thisPr,
   413  			TTL:      1,
   414  		}
   415  		r.SetLabel("@", domain)
   416  		r.SetTarget(fmt.Sprintf("%s,%s,%d,%d", // $FROM,$TO,$PRIO,$CODE
   417  			pr.Targets[0].Constraint.Value,
   418  			pr.ForwardingInfo.URL,
   419  			pr.Priority,
   420  			pr.ForwardingInfo.StatusCode))
   421  		recs = append(recs, r)
   422  	}
   423  	return recs, nil
   424  }
   425  
   426  func (c *CloudflareApi) deletePageRule(recordID, domainID string) error {
   427  	endpoint := fmt.Sprintf(singlePageRuleURL, domainID, recordID)
   428  	req, err := http.NewRequest("DELETE", endpoint, nil)
   429  	if err != nil {
   430  		return err
   431  	}
   432  	c.setHeaders(req)
   433  	_, err = handleActionResponse(http.DefaultClient.Do(req))
   434  	return err
   435  }
   436  
   437  func (c *CloudflareApi) updatePageRule(recordID, domainID string, target string) error {
   438  	if err := c.deletePageRule(recordID, domainID); err != nil {
   439  		return err
   440  	}
   441  	return c.createPageRule(domainID, target)
   442  }
   443  
   444  func (c *CloudflareApi) createPageRule(domainID string, target string) error {
   445  	endpoint := fmt.Sprintf(pageRulesURL, domainID)
   446  	return c.sendPageRule(endpoint, "POST", target)
   447  }
   448  
   449  func (c *CloudflareApi) sendPageRule(endpoint, method string, data string) error {
   450  	// from to priority code
   451  	parts := strings.Split(data, ",")
   452  	priority, _ := strconv.Atoi(parts[2])
   453  	code, _ := strconv.Atoi(parts[3])
   454  	fwdInfo := &pageRuleFwdInfo{
   455  		StatusCode: code,
   456  		URL:        parts[1],
   457  	}
   458  	dat, _ := json.Marshal(fwdInfo)
   459  	pr := &pageRule{
   460  		Status:   "active",
   461  		Priority: priority,
   462  		Targets: []pageRuleTarget{
   463  			{Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: parts[0]}},
   464  		},
   465  		Actions: []pageRuleAction{
   466  			{ID: "forwarding_url", Value: json.RawMessage(dat)},
   467  		},
   468  	}
   469  	buf := &bytes.Buffer{}
   470  	enc := json.NewEncoder(buf)
   471  	if err := enc.Encode(pr); err != nil {
   472  		return err
   473  	}
   474  	req, err := http.NewRequest(method, endpoint, buf)
   475  	if err != nil {
   476  		return err
   477  	}
   478  	c.setHeaders(req)
   479  	_, err = handleActionResponse(http.DefaultClient.Do(req))
   480  	return err
   481  }
   482  
   483  func stringifyErrors(errors []interface{}) string {
   484  	dat, err := json.Marshal(errors)
   485  	if err != nil {
   486  		return "???"
   487  	}
   488  	return string(dat)
   489  }
   490  
   491  type recordsResponse struct {
   492  	basicResponse
   493  	Result     []*cfRecord `json:"result"`
   494  	ResultInfo pagingInfo  `json:"result_info"`
   495  }
   496  
   497  type basicResponse struct {
   498  	Success  bool          `json:"success"`
   499  	Errors   []interface{} `json:"errors"`
   500  	Messages []interface{} `json:"messages"`
   501  	Result   struct {
   502  		ID string `json:"id"`
   503  	} `json:"result"`
   504  }
   505  
   506  type pageRuleResponse struct {
   507  	basicResponse
   508  	Result     []*pageRule `json:"result"`
   509  	ResultInfo pagingInfo  `json:"result_info"`
   510  }
   511  
   512  type pageRule struct {
   513  	ID             string           `json:"id,omitempty"`
   514  	Targets        []pageRuleTarget `json:"targets"`
   515  	Actions        []pageRuleAction `json:"actions"`
   516  	Priority       int              `json:"priority"`
   517  	Status         string           `json:"status"`
   518  	ModifiedOn     time.Time        `json:"modified_on,omitempty"`
   519  	CreatedOn      time.Time        `json:"created_on,omitempty"`
   520  	ForwardingInfo *pageRuleFwdInfo `json:"-"`
   521  }
   522  
   523  type pageRuleTarget struct {
   524  	Target     string             `json:"target"`
   525  	Constraint pageRuleConstraint `json:"constraint"`
   526  }
   527  
   528  type pageRuleConstraint struct {
   529  	Operator string `json:"operator"`
   530  	Value    string `json:"value"`
   531  }
   532  
   533  type pageRuleAction struct {
   534  	ID    string          `json:"id"`
   535  	Value json.RawMessage `json:"value"`
   536  }
   537  
   538  type pageRuleFwdInfo struct {
   539  	URL        string `json:"url"`
   540  	StatusCode int    `json:"status_code"`
   541  }
   542  
   543  type zoneResponse struct {
   544  	basicResponse
   545  	Result []struct {
   546  		ID          string   `json:"id"`
   547  		Name        string   `json:"name"`
   548  		Nameservers []string `json:"name_servers"`
   549  	} `json:"result"`
   550  	ResultInfo pagingInfo `json:"result_info"`
   551  }
   552  
   553  type pagingInfo struct {
   554  	Page       int `json:"page"`
   555  	PerPage    int `json:"per_page"`
   556  	Count      int `json:"count"`
   557  	TotalCount int `json:"total_count"`
   558  }