1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

CLOUDFLARE: Use cloudflare-go (#1267)

* First pass at moving to cloudflare-go vs hand made implementation of cloudflare's API

* Final changes to use cloudflare-go

* Fix for proxy configuration failing

Forgot to set the ID when we created a new records.  This didn't fail in the integrations tests so I missed it.

* Add integration test

To prevent something like what I did from happening in the future.

* Fix bad messaging
This commit is contained in:
Brian Hartvigsen
2021-09-30 05:09:42 -06:00
committed by GitHub
parent 414f57274d
commit d8941a04bc
5 changed files with 121 additions and 390 deletions

1
go.mod
View File

@ -18,6 +18,7 @@ require (
github.com/billputer/go-namecheap v0.0.0-20210108011502-994a912fb7f9 github.com/billputer/go-namecheap v0.0.0-20210108011502-994a912fb7f9
github.com/boombuler/barcode v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cloudflare/cloudflare-go v0.24.0
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/daaku/go.zipexe v1.0.1 // indirect github.com/daaku/go.zipexe v1.0.1 // indirect
github.com/digitalocean/godo v1.65.0 github.com/digitalocean/godo v1.65.0

5
go.sum
View File

@ -133,6 +133,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.24.0 h1:ij4wyHWiBx2YXuqkDPQo17WkpbEGBvra5ipWT7PWwig=
github.com/cloudflare/cloudflare-go v0.24.0/go.mod h1:sPWL/lIC6biLEdyGZwBQ1rGQKF1FhM7N60fuNiFdYTI=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@ -468,6 +470,7 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
@ -500,6 +503,7 @@ github.com/nrdcg/goinwx v0.8.1 h1:20EQ/JaGFnSKwiDH2JzjIpicffl3cPk6imJBDqVBVtU=
github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c= github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
@ -728,6 +732,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw= golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=

View File

@ -385,6 +385,20 @@ func cfRedirTemp(pattern, target string) *models.RecordConfig {
return r return r
} }
func cfProxyA(name, target, status string) *models.RecordConfig {
r := a(name, target)
r.Metadata = make(map[string]string)
r.Metadata["cloudflare_proxy"] = status
return r
}
func cfProxyCNAME(name, target, status string) *models.RecordConfig {
r := cname(name, target)
r.Metadata = make(map[string]string)
r.Metadata["cloudflare_proxy"] = status
return r
}
func ns(name, target string) *models.RecordConfig { func ns(name, target string) *models.RecordConfig {
return makeRec(name, target, "NS") return makeRec(name, target, "NS")
} }
@ -1359,6 +1373,17 @@ func makeTests(t *testing.T) []*TestGroup {
// cfRedirTemp("nytimes.**current-domain-no-trailing**/*", "https://www.nytimes.com/$1"), // cfRedirTemp("nytimes.**current-domain-no-trailing**/*", "https://www.nytimes.com/$1"),
//), //),
), ),
testgroup("CF_PROXY",
only("CLOUDFLAREAPI"),
tc("proxyon", cfProxyA("proxyme", "1.2.3.4", "on")),
tc("proxychangetarget", cfProxyA("proxyme", "1.2.3.5", "on")),
tc("proxychangeproxy", cfProxyA("proxyme", "1.2.3.5", "off")),
clear(),
tc("proxycname", cfProxyCNAME("anewproxy", "example.com.", "on")),
tc("proxycnamechange", cfProxyCNAME("anewproxy", "example.com.", "off")),
clear(),
),
} }
return tests return tests

View File

@ -6,8 +6,8 @@ import (
"log" "log"
"net" "net"
"strings" "strings"
"time"
"github.com/cloudflare/cloudflare-go"
"github.com/miekg/dns/dnsutil" "github.com/miekg/dns/dnsutil"
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
@ -73,6 +73,7 @@ type cloudflareProvider struct {
ipConversions []transform.IPConversion ipConversions []transform.IPConversion
ignoredLabels []string ignoredLabels []string
manageRedirects bool manageRedirects bool
cfClient *cloudflare.API
} }
func labelMatches(label string, matches []string) bool { func labelMatches(label string, matches []string) bool {
@ -172,8 +173,8 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
for i := len(records) - 1; i >= 0; i-- { for i := len(records) - 1; i >= 0; i-- {
rec := records[i] rec := records[i]
// Delete ignore labels // Delete ignore labels
if labelMatches(dnsutil.TrimDomainName(rec.Original.(*cfRecord).Name, dc.Name), c.ignoredLabels) { if labelMatches(dnsutil.TrimDomainName(rec.Original.(cloudflare.DNSRecord).Name, dc.Name), c.ignoredLabels) {
printer.Debugf("ignored_label: %s\n", rec.Original.(*cfRecord).Name) printer.Debugf("ignored_label: %s\n", rec.Original.(cloudflare.DNSRecord).Name)
records = append(records[:i], records[i+1:]...) records = append(records[:i], records[i+1:]...)
} }
} }
@ -223,10 +224,10 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
if ex.Type == "PAGE_RULE" { if ex.Type == "PAGE_RULE" {
corrections = append(corrections, &models.Correction{ corrections = append(corrections, &models.Correction{
Msg: d.String(), Msg: d.String(),
F: func() error { return c.deletePageRule(ex.Original.(*pageRule).ID, id) }, F: func() error { return c.deletePageRule(ex.Original.(cloudflare.PageRule).ID, id) },
}) })
} else { } else {
corr := c.deleteRec(ex.Original.(*cfRecord), id) corr := c.deleteRec(ex.Original.(cloudflare.DNSRecord), id)
// DS records must always have a corresponding NS record. // DS records must always have a corresponding NS record.
// Therefore, we remove DS records before any NS records. // Therefore, we remove DS records before any NS records.
if d.Existing.Type == "DS" { if d.Existing.Type == "DS" {
@ -261,10 +262,10 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
if rec.Type == "PAGE_RULE" { if rec.Type == "PAGE_RULE" {
corrections = append(corrections, &models.Correction{ corrections = append(corrections, &models.Correction{
Msg: d.String(), Msg: d.String(),
F: func() error { return c.updatePageRule(ex.Original.(*pageRule).ID, id, rec.GetTargetField()) }, F: func() error { return c.updatePageRule(ex.Original.(cloudflare.PageRule).ID, id, rec.GetTargetField()) },
}) })
} else { } else {
e := ex.Original.(*cfRecord) e := ex.Original.(cloudflare.DNSRecord)
proxy := e.Proxiable && rec.Metadata[metaProxy] != "off" proxy := e.Proxiable && rec.Metadata[metaProxy] != "off"
corrections = append(corrections, &models.Correction{ corrections = append(corrections, &models.Correction{
Msg: d.String(), Msg: d.String(),
@ -458,6 +459,17 @@ func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNS
return nil, fmt.Errorf("if cloudflare apitoken is set, apikey and apiuser should not be provided") return nil, fmt.Errorf("if cloudflare apitoken is set, apikey and apiuser should not be provided")
} }
var err error
if api.APIToken != "" {
api.cfClient, err = cloudflare.NewWithAPIToken(api.APIToken)
} else {
api.cfClient, err = cloudflare.New(api.APIKey, api.APIUser)
}
if err != nil {
return nil, fmt.Errorf("cloudflare credentials: %w", err)
}
// Check account data if set // Check account data if set
api.AccountID, api.AccountName = m["accountid"], m["accountname"] api.AccountID, api.AccountName = m["accountid"], m["accountname"]
if (api.AccountID != "" && api.AccountName == "") || (api.AccountID == "" && api.AccountName != "") { if (api.AccountID != "" && api.AccountName == "") || (api.AccountID == "" && api.AccountName != "") {
@ -558,33 +570,16 @@ func (c cfTarget) FQDN() string {
return strings.TrimRight(string(c), ".") + "." return strings.TrimRight(string(c), ".") + "."
} }
type cfRecord struct { func (cfp *cloudflareProvider) nativeToRecord(domain string, c cloudflare.DNSRecord) (*models.RecordConfig, error) {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Content string `json:"content"`
Proxiable bool `json:"proxiable"`
Proxied bool `json:"proxied"`
TTL uint32 `json:"ttl"`
Locked bool `json:"locked"`
ZoneID string `json:"zone_id"`
ZoneName string `json:"zone_name"`
CreatedOn time.Time `json:"created_on"`
ModifiedOn time.Time `json:"modified_on"`
Data *cfRecData `json:"data"`
Priority json.Number `json:"priority"`
}
func (c *cfRecord) nativeToRecord(domain string) (*models.RecordConfig, error) {
// normalize cname,mx,ns records with dots to be consistent with our config format. // normalize cname,mx,ns records with dots to be consistent with our config format.
if c.Type == "CNAME" || c.Type == "MX" || c.Type == "NS" || c.Type == "SRV" { if c.Type == "CNAME" || c.Type == "MX" || c.Type == "NS" {
if c.Content != "." { if c.Content != "." {
c.Content = c.Content + "." c.Content = c.Content + "."
} }
} }
rc := &models.RecordConfig{ rc := &models.RecordConfig{
TTL: c.TTL, TTL: uint32(c.TTL),
Original: c, Original: c,
} }
rc.SetLabelFromFQDN(c.Name, domain) rc.SetLabelFromFQDN(c.Name, domain)
@ -596,23 +591,17 @@ func (c *cfRecord) nativeToRecord(domain string) (*models.RecordConfig, error) {
switch rType := c.Type; rType { // #rtype_variations switch rType := c.Type; rType { // #rtype_variations
case "MX": case "MX":
var priority uint16 if err := rc.SetTargetMX(*c.Priority, c.Content); err != nil {
if c.Priority == "" {
priority = 0
} else {
p, err := c.Priority.Int64()
if err != nil {
return nil, fmt.Errorf("error decoding priority from cloudflare record: %w", err)
}
priority = uint16(p)
}
if err := rc.SetTargetMX(priority, c.Content); err != nil {
return nil, fmt.Errorf("unparsable MX record received from cloudflare: %w", err) return nil, fmt.Errorf("unparsable MX record received from cloudflare: %w", err)
} }
case "SRV": case "SRV":
data := *c.Data data := c.Data.(map[string]interface{})
if err := rc.SetTargetSRV(data.Priority, data.Weight, data.Port, target := data["target"].(string)
dnsutil.AddOrigin(data.Target.FQDN(), domain)); err != nil { if target != "." {
target += "."
}
if err := rc.SetTargetSRV(uint16(data["priority"].(float64)), uint16(data["weight"].(float64)), uint16(data["port"].(float64)),
target); err != nil {
return nil, fmt.Errorf("unparsable SRV record received from cloudflare: %w", err) return nil, fmt.Errorf("unparsable SRV record received from cloudflare: %w", err)
} }
default: // "A", "AAAA", "ANAME", "CAA", "CNAME", "NS", "PTR", "TXT" default: // "A", "AAAA", "ANAME", "CAA", "CNAME", "NS", "PTR", "TXT"
@ -630,7 +619,7 @@ func getProxyMetadata(r *models.RecordConfig) map[string]string {
} }
var proxied bool var proxied bool
if r.Original != nil { if r.Original != nil {
proxied = r.Original.(*cfRecord).Proxied proxied = *r.Original.(cloudflare.DNSRecord).Proxied
} else { } else {
proxied = r.Metadata[metaProxy] != "off" proxied = r.Metadata[metaProxy] != "off"
} }

View File

@ -1,131 +1,63 @@
package cloudflare package cloudflare
import ( import (
"bytes" "context"
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/models"
) "github.com/cloudflare/cloudflare-go"
const (
baseURL = "https://api.cloudflare.com/client/v4/"
zonesURL = baseURL + "zones/"
recordsURL = zonesURL + "%s/dns_records/"
pageRulesURL = zonesURL + "%s/pagerules/"
singlePageRuleURL = pageRulesURL + "%s"
singleRecordURL = recordsURL + "%s"
) )
// get list of domains for account. Cache so the ids can be looked up from domain name // get list of domains for account. Cache so the ids can be looked up from domain name
func (c *cloudflareProvider) fetchDomainList() error { func (c *cloudflareProvider) fetchDomainList() error {
c.domainIndex = map[string]string{} c.domainIndex = map[string]string{}
c.nameservers = map[string][]string{} c.nameservers = map[string][]string{}
page := 1 zones, err := c.cfClient.ListZones(context.Background())
for { if err != nil {
zr := &zoneResponse{}
url := fmt.Sprintf("%s?page=%d&per_page=50", zonesURL, page)
if err := c.get(url, zr); err != nil {
return fmt.Errorf("failed fetching domain list from cloudflare: %s", err) return fmt.Errorf("failed fetching domain list from cloudflare: %s", err)
} }
if !zr.Success {
return fmt.Errorf("failed fetching domain list from cloudflare: %s", stringifyErrors(zr.Errors)) for _, zone := range zones {
}
for _, zone := range zr.Result {
c.domainIndex[zone.Name] = zone.ID c.domainIndex[zone.Name] = zone.ID
c.nameservers[zone.Name] = append(c.nameservers[zone.Name], zone.Nameservers...) c.nameservers[zone.Name] = append(c.nameservers[zone.Name], zone.NameServers...)
}
ri := zr.ResultInfo
if len(zr.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount {
break
}
page++
} }
return nil return nil
} }
// get all records for a domain // get all records for a domain
func (c *cloudflareProvider) getRecordsForDomain(id string, domain string) ([]*models.RecordConfig, error) { func (c *cloudflareProvider) getRecordsForDomain(id string, domain string) ([]*models.RecordConfig, error) {
url := fmt.Sprintf(recordsURL, id)
page := 1
records := []*models.RecordConfig{} records := []*models.RecordConfig{}
for { rrs, err := c.cfClient.DNSRecords(context.Background(), id, cloudflare.DNSRecord{})
reqURL := fmt.Sprintf("%s?page=%d&per_page=100", url, page) if err != nil {
var data recordsResponse
if err := c.get(reqURL, &data); err != nil {
return nil, fmt.Errorf("failed fetching record list from cloudflare: %s", err) return nil, fmt.Errorf("failed fetching record list from cloudflare: %s", err)
} }
if !data.Success { for _, rec := range rrs {
return nil, fmt.Errorf("failed fetching record list cloudflare: %s", stringifyErrors(data.Errors)) rt, err := c.nativeToRecord(domain, rec)
}
for _, rec := range data.Result {
rt, err := rec.nativeToRecord(domain)
if err != nil { if err != nil {
return nil, err return nil, err
} }
records = append(records, rt) records = append(records, rt)
} }
ri := data.ResultInfo
if len(data.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount {
break
}
page++
}
return records, nil return records, nil
} }
// create a correction to delete a record // create a correction to delete a record
func (c *cloudflareProvider) deleteRec(rec *cfRecord, domainID string) *models.Correction { func (c *cloudflareProvider) deleteRec(rec cloudflare.DNSRecord, domainID string) *models.Correction {
return &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), Msg: fmt.Sprintf("DELETE record: %s %s %d %s (id=%s)", rec.Name, rec.Type, rec.TTL, rec.Content, rec.ID),
F: func() error { F: func() error {
endpoint := fmt.Sprintf(singleRecordURL, domainID, rec.ID) err := c.cfClient.DeleteDNSRecord(context.Background(), 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 return err
}, },
} }
} }
func (c *cloudflareProvider) createZone(domainName string) (string, error) { func (c *cloudflareProvider) createZone(domainName string) (string, error) {
type createZone struct { zone, err := c.cfClient.CreateZone(context.Background(), domainName, false, cloudflare.Account{Name: c.AccountName, ID: c.AccountID}, "full")
Name string `json:"name"` return zone.ID, err
Account struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"account"`
}
var id string
cz := &createZone{
Name: domainName}
if c.AccountID != "" || c.AccountName != "" {
cz.Account.ID = c.AccountID
cz.Account.Name = c.AccountName
}
buf := &bytes.Buffer{}
encoder := json.NewEncoder(buf)
if err := encoder.Encode(cz); err != nil {
return "", err
}
req, err := http.NewRequest("POST", zonesURL, buf)
if err != nil {
return "", err
}
c.setHeaders(req)
id, err = handleActionResponse(http.DefaultClient.Do(req))
return id, err
} }
func cfDSData(rec *models.RecordConfig) *cfRecData { func cfDSData(rec *models.RecordConfig) *cfRecData {
@ -177,14 +109,6 @@ func cfSshfpData(rec *models.RecordConfig) *cfRecData {
} }
func (c *cloudflareProvider) createRec(rec *models.RecordConfig, domainID string) []*models.Correction { func (c *cloudflareProvider) 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"`
Data *cfRecData `json:"data"`
}
var id string var id string
content := rec.GetTargetField() content := rec.GetTargetField()
if rec.Metadata[metaOriginalIP] != "" { if rec.Metadata[metaOriginalIP] != "" {
@ -203,13 +127,12 @@ func (c *cloudflareProvider) createRec(rec *models.RecordConfig, domainID string
arr := []*models.Correction{{ arr := []*models.Correction{{
Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.GetLabel(), rec.Type, rec.TTL, prio, content), Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.GetLabel(), rec.Type, rec.TTL, prio, content),
F: func() error { F: func() error {
cf := cloudflare.DNSRecord{
cf := &createRecord{
Name: rec.GetLabel(), Name: rec.GetLabel(),
Type: rec.Type, Type: rec.Type,
TTL: rec.TTL, TTL: int(rec.TTL),
Content: content, Content: content,
Priority: rec.MxPreference, Priority: &rec.MxPreference,
} }
if rec.Type == "SRV" { if rec.Type == "SRV" {
cf.Data = cfSrvData(rec) cf.Data = cfSrvData(rec)
@ -227,18 +150,8 @@ func (c *cloudflareProvider) createRec(rec *models.RecordConfig, domainID string
} else if rec.Type == "DS" { } else if rec.Type == "DS" {
cf.Data = cfDSData(rec) cf.Data = cfDSData(rec)
} }
endpoint := fmt.Sprintf(recordsURL, domainID) resp, err := c.cfClient.CreateDNSRecord(context.Background(), domainID, cf)
buf := &bytes.Buffer{} id = resp.Result.ID
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 return err
}, },
}} }}
@ -255,25 +168,15 @@ func (c *cloudflareProvider) modifyRecord(domainID, recID string, proxied bool,
if domainID == "" || recID == "" { if domainID == "" || recID == "" {
return fmt.Errorf("cannot modify record if domain or record id are empty") return fmt.Errorf("cannot modify record if domain or record id are empty")
} }
type record struct {
ID string `json:"id"` r := cloudflare.DNSRecord{
Proxied bool `json:"proxied"`
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
Priority uint16 `json:"priority"`
TTL uint32 `json:"ttl"`
Data *cfRecData `json:"data"`
}
r := record{
ID: recID, ID: recID,
Proxied: proxied, Proxied: &proxied,
Name: rec.GetLabel(), Name: rec.GetLabel(),
Type: rec.Type, Type: rec.Type,
Content: rec.GetTargetField(), Content: rec.GetTargetField(),
Priority: rec.MxPreference, Priority: &rec.MxPreference,
TTL: rec.TTL, TTL: int(rec.TTL),
Data: nil,
} }
if rec.Type == "TXT" { if rec.Type == "TXT" {
if len(rec.TxtStrings) > 1 { if len(rec.TxtStrings) > 1 {
@ -297,129 +200,28 @@ func (c *cloudflareProvider) modifyRecord(domainID, recID string, proxied bool,
r.Data = cfDSData(rec) r.Data = cfDSData(rec)
r.Content = "" r.Content = ""
} }
endpoint := fmt.Sprintf(singleRecordURL, domainID, recID) return c.cfClient.UpdateDNSRecord(context.Background(), domainID, recID, r)
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
} }
// change universal ssl state // change universal ssl state
func (c *cloudflareProvider) changeUniversalSSL(domainID string, state bool) error { func (c *cloudflareProvider) changeUniversalSSL(domainID string, state bool) error {
type setUniversalSSL struct { _, err := c.cfClient.EditUniversalSSLSetting(context.Background(), domainID, cloudflare.UniversalSSLSetting{Enabled: state})
Enabled bool `json:"enabled"`
}
us := &setUniversalSSL{
Enabled: state,
}
// create json
buf := &bytes.Buffer{}
encoder := json.NewEncoder(buf)
if err := encoder.Encode(us); err != nil {
return err return err
} }
// send request. // get universal ssl state
endpoint := fmt.Sprintf(zonesURL+"%s/ssl/universal/settings", domainID)
req, err := http.NewRequest("PATCH", endpoint, buf)
if err != nil {
return err
}
c.setHeaders(req)
_, err = handleActionResponse(http.DefaultClient.Do(req))
return err
}
// change universal ssl state
func (c *cloudflareProvider) getUniversalSSL(domainID string) (bool, error) { func (c *cloudflareProvider) getUniversalSSL(domainID string) (bool, error) {
type universalSSLResponse struct { result, err := c.cfClient.UniversalSSLSettingDetails(context.Background(), domainID)
Success bool `json:"success"` return result.Enabled, err
Errors []interface{} `json:"errors"`
Messages []interface{} `json:"messages"`
Result struct {
Enabled bool `json:"enabled"`
} `json:"result"`
}
// send request.
endpoint := fmt.Sprintf(zonesURL+"%s/ssl/universal/settings", domainID)
var result universalSSLResponse
err := c.get(endpoint, &result)
if err != nil {
return true, err
}
return result.Result.Enabled, 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 *cloudflareProvider) setHeaders(req *http.Request) {
if len(c.APIToken) > 0 {
req.Header.Set("Authorization", "Bearer "+c.APIToken)
} else {
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 *cloudflareProvider) 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 {
dat, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(dat))
return fmt.Errorf("bad status code from cloudflare: %d not 200", resp.StatusCode)
}
decoder := json.NewDecoder(resp.Body)
return decoder.Decode(target)
} }
func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.RecordConfig, error) { func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.RecordConfig, error) {
url := fmt.Sprintf(pageRulesURL, id) rules, err := c.cfClient.ListPageRules(context.Background(), id)
data := pageRuleResponse{} if err != nil {
if err := c.get(url, &data); err != nil { return nil, fmt.Errorf("failed fetching page rule list cloudflare: %s", err)
return nil, fmt.Errorf("failed fetching page rule list from cloudflare: %s", err)
}
if !data.Success {
return nil, fmt.Errorf("failed fetching page rule list cloudflare: %s", stringifyErrors(data.Errors))
} }
recs := []*models.RecordConfig{} recs := []*models.RecordConfig{}
for _, pr := range data.Result { for _, pr := range rules {
// only interested in forwarding rules. Lets be very specific, and skip anything else // only interested in forwarding rules. Lets be very specific, and skip anything else
if len(pr.Actions) != 1 || len(pr.Targets) != 1 { if len(pr.Actions) != 1 || len(pr.Targets) != 1 {
continue continue
@ -427,10 +229,7 @@ func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.R
if pr.Actions[0].ID != "forwarding_url" { if pr.Actions[0].ID != "forwarding_url" {
continue continue
} }
err := json.Unmarshal([]byte(pr.Actions[0].Value), &pr.ForwardingInfo) value := pr.Actions[0].Value.(map[string]interface{})
if err != nil {
return nil, err
}
var thisPr = pr var thisPr = pr
r := &models.RecordConfig{ r := &models.RecordConfig{
Type: "PAGE_RULE", Type: "PAGE_RULE",
@ -440,26 +239,21 @@ func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.R
r.SetLabel("@", domain) r.SetLabel("@", domain)
r.SetTarget(fmt.Sprintf("%s,%s,%d,%d", // $FROM,$TO,$PRIO,$CODE r.SetTarget(fmt.Sprintf("%s,%s,%d,%d", // $FROM,$TO,$PRIO,$CODE
pr.Targets[0].Constraint.Value, pr.Targets[0].Constraint.Value,
pr.ForwardingInfo.URL, value["url"],
pr.Priority, pr.Priority,
pr.ForwardingInfo.StatusCode)) int(value["status_code"].(float64))))
recs = append(recs, r) recs = append(recs, r)
} }
return recs, nil return recs, nil
} }
func (c *cloudflareProvider) deletePageRule(recordID, domainID string) error { func (c *cloudflareProvider) deletePageRule(recordID, domainID string) error {
endpoint := fmt.Sprintf(singlePageRuleURL, domainID, recordID) return c.cfClient.DeletePageRule(context.Background(), domainID, recordID)
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 *cloudflareProvider) updatePageRule(recordID, domainID string, target string) error { func (c *cloudflareProvider) updatePageRule(recordID, domainID string, target string) error {
// maybe someday?
//c.apiProvider.UpdatePageRule(context.Background(), domainId, recordID, )
if err := c.deletePageRule(recordID, domainID); err != nil { if err := c.deletePageRule(recordID, domainID); err != nil {
return err return err
} }
@ -467,117 +261,34 @@ func (c *cloudflareProvider) updatePageRule(recordID, domainID string, target st
} }
func (c *cloudflareProvider) createPageRule(domainID string, target string) error { func (c *cloudflareProvider) createPageRule(domainID string, target string) error {
endpoint := fmt.Sprintf(pageRulesURL, domainID)
return c.sendPageRule(endpoint, "POST", target)
}
func (c *cloudflareProvider) sendPageRule(endpoint, method string, data string) error {
// from to priority code // from to priority code
parts := strings.Split(data, ",") parts := strings.Split(target, ",")
priority, _ := strconv.Atoi(parts[2]) priority, _ := strconv.Atoi(parts[2])
code, _ := strconv.Atoi(parts[3]) code, _ := strconv.Atoi(parts[3])
fwdInfo := &pageRuleFwdInfo{ pr := cloudflare.PageRule{
StatusCode: code,
URL: parts[1],
}
dat, _ := json.Marshal(fwdInfo)
pr := &pageRule{
Status: "active", Status: "active",
Priority: priority, Priority: priority,
Targets: []pageRuleTarget{ Targets: []cloudflare.PageRuleTarget{
{Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: parts[0]}}, {Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: parts[0]}},
}, },
Actions: []pageRuleAction{ Actions: []cloudflare.PageRuleAction{
{ID: "forwarding_url", Value: json.RawMessage(dat)}, {ID: "forwarding_url", Value: &pageRuleFwdInfo{
StatusCode: code,
URL: parts[1],
}},
}, },
} }
buf := &bytes.Buffer{} _, err := c.cfClient.CreatePageRule(context.Background(), domainID, pr)
enc := json.NewEncoder(buf)
if err := enc.Encode(pr); err != nil {
return err
}
req, err := http.NewRequest(method, endpoint, buf)
if err != nil {
return err
}
c.setHeaders(req)
_, err = handleActionResponse(http.DefaultClient.Do(req))
return err return err
} }
func stringifyErrors(errors []interface{}) string { // go-staticcheck lies!
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 pageRuleResponse struct {
basicResponse
Result []*pageRule `json:"result"`
ResultInfo pagingInfo `json:"result_info"`
}
type pageRule struct {
ID string `json:"id,omitempty"`
Targets []pageRuleTarget `json:"targets"`
Actions []pageRuleAction `json:"actions"`
Priority int `json:"priority"`
Status string `json:"status"`
ModifiedOn time.Time `json:"modified_on,omitempty"`
CreatedOn time.Time `json:"created_on,omitempty"`
ForwardingInfo *pageRuleFwdInfo `json:"-"`
}
type pageRuleTarget struct {
Target string `json:"target"`
Constraint pageRuleConstraint `json:"constraint"`
}
type pageRuleConstraint struct { type pageRuleConstraint struct {
Operator string `json:"operator"` Operator string `json:"operator"`
Value string `json:"value"` Value string `json:"value"`
} }
type pageRuleAction struct {
ID string `json:"id"`
Value json.RawMessage `json:"value"`
}
type pageRuleFwdInfo struct { type pageRuleFwdInfo struct {
URL string `json:"url"` URL string `json:"url"`
StatusCode int `json:"status_code"` StatusCode int `json:"status_code"`
} }
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"`
}