mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-12-28 19:23:07 +08:00
12f006441b
* initial refactoring of diffing * making cloudflare and others compile * gandi and gcloud. no idea if gandi works anymore. * r53 * namedotcom wasn't working.
250 lines
6.9 KiB
Go
250 lines
6.9 KiB
Go
package cloudflare
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/StackExchange/dnscontrol/models"
|
|
)
|
|
|
|
const (
|
|
baseURL = "https://api.cloudflare.com/client/v4/"
|
|
zonesURL = baseURL + "zones/"
|
|
recordsURL = zonesURL + "%s/dns_records/"
|
|
singleRecordURL = recordsURL + "%s"
|
|
)
|
|
|
|
// get list of domains for account. Cache so the ids can be looked up from domain name
|
|
func (c *CloudflareApi) fetchDomainList() error {
|
|
c.domainIndex = map[string]string{}
|
|
c.nameservers = map[string][]string{}
|
|
page := 1
|
|
for {
|
|
zr := &zoneResponse{}
|
|
url := fmt.Sprintf("%s?page=%d&per_page=50", zonesURL, page)
|
|
if err := c.get(url, zr); err != nil {
|
|
return fmt.Errorf("Error fetching domain list from cloudflare: %s", err)
|
|
}
|
|
if !zr.Success {
|
|
return fmt.Errorf("Error fetching domain list from cloudflare: %s", stringifyErrors(zr.Errors))
|
|
}
|
|
for _, zone := range zr.Result {
|
|
c.domainIndex[zone.Name] = zone.ID
|
|
for _, ns := range zone.Nameservers {
|
|
c.nameservers[zone.Name] = append(c.nameservers[zone.Name], ns)
|
|
}
|
|
}
|
|
ri := zr.ResultInfo
|
|
if len(zr.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount {
|
|
break
|
|
}
|
|
page++
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// get all records for a domain
|
|
func (c *CloudflareApi) getRecordsForDomain(id string, domain string) ([]*models.RecordConfig, error) {
|
|
url := fmt.Sprintf(recordsURL, id)
|
|
page := 1
|
|
records := []*models.RecordConfig{}
|
|
for {
|
|
reqURL := fmt.Sprintf("%s?page=%d&per_page=100", url, page)
|
|
var data recordsResponse
|
|
if err := c.get(reqURL, &data); err != nil {
|
|
return nil, fmt.Errorf("Error fetching record list from cloudflare: %s", err)
|
|
}
|
|
if !data.Success {
|
|
return nil, fmt.Errorf("Error fetching record list cloudflare: %s", stringifyErrors(data.Errors))
|
|
}
|
|
for _, rec := range data.Result {
|
|
records = append(records, rec.toRecord(domain))
|
|
}
|
|
ri := data.ResultInfo
|
|
if len(data.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount {
|
|
break
|
|
}
|
|
page++
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
// create a correction to delete a record
|
|
func (c *CloudflareApi) deleteRec(rec *cfRecord, domainID string) *models.Correction {
|
|
return &models.Correction{
|
|
Msg: fmt.Sprintf("DELETE record: %s %s %d %s (id=%s)", rec.Name, rec.Type, rec.TTL, rec.Content, rec.ID),
|
|
F: func() error {
|
|
endpoint := fmt.Sprintf(singleRecordURL, domainID, rec.ID)
|
|
req, err := http.NewRequest("DELETE", endpoint, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.setHeaders(req)
|
|
_, err = handleActionResponse(http.DefaultClient.Do(req))
|
|
return err
|
|
},
|
|
}
|
|
}
|
|
|
|
func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*models.Correction {
|
|
type createRecord struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Content string `json:"content"`
|
|
TTL uint32 `json:"ttl"`
|
|
Priority uint16 `json:"priority"`
|
|
}
|
|
var id string
|
|
content := rec.Target
|
|
if rec.Metadata[metaOriginalIP] != "" {
|
|
content = rec.Metadata[metaOriginalIP]
|
|
}
|
|
prio := ""
|
|
if rec.Type == "MX" {
|
|
prio = fmt.Sprintf(" %d ", rec.Priority)
|
|
}
|
|
arr := []*models.Correction{{
|
|
Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.Name, rec.Type, rec.TTL, prio, content),
|
|
F: func() error {
|
|
|
|
cf := &createRecord{
|
|
Name: rec.Name,
|
|
Type: rec.Type,
|
|
TTL: rec.TTL,
|
|
Content: content,
|
|
Priority: rec.Priority,
|
|
}
|
|
endpoint := fmt.Sprintf(recordsURL, domainID)
|
|
buf := &bytes.Buffer{}
|
|
encoder := json.NewEncoder(buf)
|
|
if err := encoder.Encode(cf); err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequest("POST", endpoint, buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.setHeaders(req)
|
|
id, err = handleActionResponse(http.DefaultClient.Do(req))
|
|
return err
|
|
},
|
|
}}
|
|
if rec.Metadata[metaProxy] != "off" {
|
|
arr = append(arr, &models.Correction{
|
|
Msg: fmt.Sprintf("ACTIVATE PROXY for new record %s %s %d %s", rec.Name, rec.Type, rec.TTL, rec.Target),
|
|
F: func() error { return c.modifyRecord(domainID, id, true, rec) },
|
|
})
|
|
}
|
|
return arr
|
|
}
|
|
|
|
func (c *CloudflareApi) modifyRecord(domainID, recID string, proxied bool, rec *models.RecordConfig) error {
|
|
if domainID == "" || recID == "" {
|
|
return fmt.Errorf("Cannot modify record if domain or record id are empty.")
|
|
}
|
|
type record struct {
|
|
ID string `json:"id"`
|
|
Proxied bool `json:"proxied"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Content string `json:"content"`
|
|
Priority uint16 `json:"priority"`
|
|
TTL uint32 `json:"ttl"`
|
|
}
|
|
r := record{recID, proxied, rec.Name, rec.Type, rec.Target, rec.Priority, rec.TTL}
|
|
endpoint := fmt.Sprintf(singleRecordURL, domainID, recID)
|
|
buf := &bytes.Buffer{}
|
|
encoder := json.NewEncoder(buf)
|
|
if err := encoder.Encode(r); err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequest("PUT", endpoint, buf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.setHeaders(req)
|
|
_, err = handleActionResponse(http.DefaultClient.Do(req))
|
|
return err
|
|
}
|
|
|
|
// common error handling for all action responses
|
|
func handleActionResponse(resp *http.Response, err error) (id string, e error) {
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
result := &basicResponse{}
|
|
decoder := json.NewDecoder(resp.Body)
|
|
if err = decoder.Decode(result); err != nil {
|
|
return "", fmt.Errorf("Unknown error. Status code: %d", resp.StatusCode)
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
return "", fmt.Errorf(stringifyErrors(result.Errors))
|
|
}
|
|
return result.Result.ID, nil
|
|
}
|
|
|
|
func (c *CloudflareApi) setHeaders(req *http.Request) {
|
|
req.Header.Set("X-Auth-Key", c.ApiKey)
|
|
req.Header.Set("X-Auth-Email", c.ApiUser)
|
|
}
|
|
|
|
// generic get handler. makes request and unmarshalls response to given interface
|
|
func (c *CloudflareApi) get(endpoint string, target interface{}) error {
|
|
req, err := http.NewRequest("GET", endpoint, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.setHeaders(req)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
return fmt.Errorf("Bad status code from cloudflare: %d not 200.", resp.StatusCode)
|
|
}
|
|
decoder := json.NewDecoder(resp.Body)
|
|
return decoder.Decode(target)
|
|
}
|
|
|
|
func stringifyErrors(errors []interface{}) string {
|
|
dat, err := json.Marshal(errors)
|
|
if err != nil {
|
|
return "???"
|
|
}
|
|
return string(dat)
|
|
}
|
|
|
|
type recordsResponse struct {
|
|
basicResponse
|
|
Result []*cfRecord `json:"result"`
|
|
ResultInfo pagingInfo `json:"result_info"`
|
|
}
|
|
type basicResponse struct {
|
|
Success bool `json:"success"`
|
|
Errors []interface{} `json:"errors"`
|
|
Messages []interface{} `json:"messages"`
|
|
Result struct {
|
|
ID string `json:"id"`
|
|
} `json:"result"`
|
|
}
|
|
|
|
type zoneResponse struct {
|
|
basicResponse
|
|
Result []struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Nameservers []string `json:"name_servers"`
|
|
} `json:"result"`
|
|
ResultInfo pagingInfo `json:"result_info"`
|
|
}
|
|
|
|
type pagingInfo struct {
|
|
Page int `json:"page"`
|
|
PerPage int `json:"per_page"`
|
|
Count int `json:"count"`
|
|
TotalCount int `json:"total_count"`
|
|
}
|