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:
1
go.mod
1
go.mod
@ -18,6 +18,7 @@ require (
|
||||
github.com/billputer/go-namecheap v0.0.0-20210108011502-994a912fb7f9
|
||||
github.com/boombuler/barcode v1.0.1 // 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/daaku/go.zipexe v1.0.1 // indirect
|
||||
github.com/digitalocean/godo v1.65.0
|
||||
|
5
go.sum
5
go.sum
@ -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/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
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-20200629203442-efcf912fb354/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.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
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/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
|
||||
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/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/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.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
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-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-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-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw=
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
@ -385,6 +385,20 @@ func cfRedirTemp(pattern, target string) *models.RecordConfig {
|
||||
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 {
|
||||
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"),
|
||||
//),
|
||||
),
|
||||
|
||||
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
|
||||
|
@ -6,8 +6,8 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudflare/cloudflare-go"
|
||||
"github.com/miekg/dns/dnsutil"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
@ -73,6 +73,7 @@ type cloudflareProvider struct {
|
||||
ipConversions []transform.IPConversion
|
||||
ignoredLabels []string
|
||||
manageRedirects bool
|
||||
cfClient *cloudflare.API
|
||||
}
|
||||
|
||||
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-- {
|
||||
rec := records[i]
|
||||
// Delete ignore labels
|
||||
if labelMatches(dnsutil.TrimDomainName(rec.Original.(*cfRecord).Name, dc.Name), c.ignoredLabels) {
|
||||
printer.Debugf("ignored_label: %s\n", rec.Original.(*cfRecord).Name)
|
||||
if labelMatches(dnsutil.TrimDomainName(rec.Original.(cloudflare.DNSRecord).Name, dc.Name), c.ignoredLabels) {
|
||||
printer.Debugf("ignored_label: %s\n", rec.Original.(cloudflare.DNSRecord).Name)
|
||||
records = append(records[:i], records[i+1:]...)
|
||||
}
|
||||
}
|
||||
@ -223,10 +224,10 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
|
||||
if ex.Type == "PAGE_RULE" {
|
||||
corrections = append(corrections, &models.Correction{
|
||||
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 {
|
||||
corr := c.deleteRec(ex.Original.(*cfRecord), id)
|
||||
corr := c.deleteRec(ex.Original.(cloudflare.DNSRecord), id)
|
||||
// DS records must always have a corresponding NS record.
|
||||
// Therefore, we remove DS records before any NS records.
|
||||
if d.Existing.Type == "DS" {
|
||||
@ -261,10 +262,10 @@ func (c *cloudflareProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*m
|
||||
if rec.Type == "PAGE_RULE" {
|
||||
corrections = append(corrections, &models.Correction{
|
||||
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 {
|
||||
e := ex.Original.(*cfRecord)
|
||||
e := ex.Original.(cloudflare.DNSRecord)
|
||||
proxy := e.Proxiable && rec.Metadata[metaProxy] != "off"
|
||||
corrections = append(corrections, &models.Correction{
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
api.AccountID, api.AccountName = m["accountid"], m["accountname"]
|
||||
if (api.AccountID != "" && api.AccountName == "") || (api.AccountID == "" && api.AccountName != "") {
|
||||
@ -558,33 +570,16 @@ func (c cfTarget) FQDN() string {
|
||||
return strings.TrimRight(string(c), ".") + "."
|
||||
}
|
||||
|
||||
type cfRecord struct {
|
||||
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) {
|
||||
func (cfp *cloudflareProvider) nativeToRecord(domain string, c cloudflare.DNSRecord) (*models.RecordConfig, error) {
|
||||
// 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 != "." {
|
||||
c.Content = c.Content + "."
|
||||
}
|
||||
}
|
||||
|
||||
rc := &models.RecordConfig{
|
||||
TTL: c.TTL,
|
||||
TTL: uint32(c.TTL),
|
||||
Original: c,
|
||||
}
|
||||
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
|
||||
case "MX":
|
||||
var priority uint16
|
||||
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 {
|
||||
if err := rc.SetTargetMX(*c.Priority, c.Content); err != nil {
|
||||
return nil, fmt.Errorf("unparsable MX record received from cloudflare: %w", err)
|
||||
}
|
||||
case "SRV":
|
||||
data := *c.Data
|
||||
if err := rc.SetTargetSRV(data.Priority, data.Weight, data.Port,
|
||||
dnsutil.AddOrigin(data.Target.FQDN(), domain)); err != nil {
|
||||
data := c.Data.(map[string]interface{})
|
||||
target := data["target"].(string)
|
||||
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)
|
||||
}
|
||||
default: // "A", "AAAA", "ANAME", "CAA", "CNAME", "NS", "PTR", "TXT"
|
||||
@ -630,7 +619,7 @@ func getProxyMetadata(r *models.RecordConfig) map[string]string {
|
||||
}
|
||||
var proxied bool
|
||||
if r.Original != nil {
|
||||
proxied = r.Original.(*cfRecord).Proxied
|
||||
proxied = *r.Original.(cloudflare.DNSRecord).Proxied
|
||||
} else {
|
||||
proxied = r.Metadata[metaProxy] != "off"
|
||||
}
|
||||
|
@ -1,131 +1,63 @@
|
||||
package cloudflare
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v3/models"
|
||||
)
|
||||
|
||||
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"
|
||||
"github.com/cloudflare/cloudflare-go"
|
||||
)
|
||||
|
||||
// get list of domains for account. Cache so the ids can be looked up from domain name
|
||||
func (c *cloudflareProvider) 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("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 zr.Result {
|
||||
c.domainIndex[zone.Name] = zone.ID
|
||||
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++
|
||||
zones, err := c.cfClient.ListZones(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed fetching domain list from cloudflare: %s", err)
|
||||
}
|
||||
|
||||
for _, zone := range zones {
|
||||
c.domainIndex[zone.Name] = zone.ID
|
||||
c.nameservers[zone.Name] = append(c.nameservers[zone.Name], zone.NameServers...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// get all records for a domain
|
||||
func (c *cloudflareProvider) 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("failed fetching record list from cloudflare: %s", err)
|
||||
rrs, err := c.cfClient.DNSRecords(context.Background(), id, cloudflare.DNSRecord{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed fetching record list from cloudflare: %s", err)
|
||||
}
|
||||
for _, rec := range rrs {
|
||||
rt, err := c.nativeToRecord(domain, rec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !data.Success {
|
||||
return nil, fmt.Errorf("failed fetching record list cloudflare: %s", stringifyErrors(data.Errors))
|
||||
}
|
||||
for _, rec := range data.Result {
|
||||
rt, err := rec.nativeToRecord(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, rt)
|
||||
}
|
||||
ri := data.ResultInfo
|
||||
if len(data.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount {
|
||||
break
|
||||
}
|
||||
page++
|
||||
records = append(records, rt)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// 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{
|
||||
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))
|
||||
err := c.cfClient.DeleteDNSRecord(context.Background(), domainID, rec.ID)
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cloudflareProvider) createZone(domainName string) (string, error) {
|
||||
type createZone struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
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
|
||||
zone, err := c.cfClient.CreateZone(context.Background(), domainName, false, cloudflare.Account{Name: c.AccountName, ID: c.AccountID}, "full")
|
||||
return zone.ID, err
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
content := rec.GetTargetField()
|
||||
if rec.Metadata[metaOriginalIP] != "" {
|
||||
@ -203,13 +127,12 @@ func (c *cloudflareProvider) createRec(rec *models.RecordConfig, domainID string
|
||||
arr := []*models.Correction{{
|
||||
Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.GetLabel(), rec.Type, rec.TTL, prio, content),
|
||||
F: func() error {
|
||||
|
||||
cf := &createRecord{
|
||||
cf := cloudflare.DNSRecord{
|
||||
Name: rec.GetLabel(),
|
||||
Type: rec.Type,
|
||||
TTL: rec.TTL,
|
||||
TTL: int(rec.TTL),
|
||||
Content: content,
|
||||
Priority: rec.MxPreference,
|
||||
Priority: &rec.MxPreference,
|
||||
}
|
||||
if rec.Type == "SRV" {
|
||||
cf.Data = cfSrvData(rec)
|
||||
@ -227,18 +150,8 @@ func (c *cloudflareProvider) createRec(rec *models.RecordConfig, domainID string
|
||||
} else if rec.Type == "DS" {
|
||||
cf.Data = cfDSData(rec)
|
||||
}
|
||||
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))
|
||||
resp, err := c.cfClient.CreateDNSRecord(context.Background(), domainID, cf)
|
||||
id = resp.Result.ID
|
||||
return err
|
||||
},
|
||||
}}
|
||||
@ -255,25 +168,15 @@ func (c *cloudflareProvider) modifyRecord(domainID, recID string, proxied bool,
|
||||
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"`
|
||||
Data *cfRecData `json:"data"`
|
||||
}
|
||||
r := record{
|
||||
|
||||
r := cloudflare.DNSRecord{
|
||||
ID: recID,
|
||||
Proxied: proxied,
|
||||
Proxied: &proxied,
|
||||
Name: rec.GetLabel(),
|
||||
Type: rec.Type,
|
||||
Content: rec.GetTargetField(),
|
||||
Priority: rec.MxPreference,
|
||||
TTL: rec.TTL,
|
||||
Data: nil,
|
||||
Priority: &rec.MxPreference,
|
||||
TTL: int(rec.TTL),
|
||||
}
|
||||
if rec.Type == "TXT" {
|
||||
if len(rec.TxtStrings) > 1 {
|
||||
@ -297,129 +200,28 @@ func (c *cloudflareProvider) modifyRecord(domainID, recID string, proxied bool,
|
||||
r.Data = cfDSData(rec)
|
||||
r.Content = ""
|
||||
}
|
||||
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
|
||||
return c.cfClient.UpdateDNSRecord(context.Background(), domainID, recID, r)
|
||||
}
|
||||
|
||||
// change universal ssl state
|
||||
func (c *cloudflareProvider) changeUniversalSSL(domainID string, state bool) error {
|
||||
type setUniversalSSL struct {
|
||||
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
|
||||
}
|
||||
|
||||
// send request.
|
||||
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))
|
||||
|
||||
_, err := c.cfClient.EditUniversalSSLSetting(context.Background(), domainID, cloudflare.UniversalSSLSetting{Enabled: state})
|
||||
return err
|
||||
}
|
||||
|
||||
// change universal ssl state
|
||||
// get universal ssl state
|
||||
func (c *cloudflareProvider) getUniversalSSL(domainID string) (bool, error) {
|
||||
type universalSSLResponse struct {
|
||||
Success bool `json:"success"`
|
||||
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)
|
||||
result, err := c.cfClient.UniversalSSLSettingDetails(context.Background(), domainID)
|
||||
return result.Enabled, err
|
||||
}
|
||||
|
||||
func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.RecordConfig, error) {
|
||||
url := fmt.Sprintf(pageRulesURL, id)
|
||||
data := pageRuleResponse{}
|
||||
if err := c.get(url, &data); err != nil {
|
||||
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))
|
||||
rules, err := c.cfClient.ListPageRules(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed fetching page rule list cloudflare: %s", err)
|
||||
}
|
||||
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
|
||||
if len(pr.Actions) != 1 || len(pr.Targets) != 1 {
|
||||
continue
|
||||
@ -427,10 +229,7 @@ func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.R
|
||||
if pr.Actions[0].ID != "forwarding_url" {
|
||||
continue
|
||||
}
|
||||
err := json.Unmarshal([]byte(pr.Actions[0].Value), &pr.ForwardingInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value := pr.Actions[0].Value.(map[string]interface{})
|
||||
var thisPr = pr
|
||||
r := &models.RecordConfig{
|
||||
Type: "PAGE_RULE",
|
||||
@ -440,26 +239,21 @@ func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.R
|
||||
r.SetLabel("@", domain)
|
||||
r.SetTarget(fmt.Sprintf("%s,%s,%d,%d", // $FROM,$TO,$PRIO,$CODE
|
||||
pr.Targets[0].Constraint.Value,
|
||||
pr.ForwardingInfo.URL,
|
||||
value["url"],
|
||||
pr.Priority,
|
||||
pr.ForwardingInfo.StatusCode))
|
||||
int(value["status_code"].(float64))))
|
||||
recs = append(recs, r)
|
||||
}
|
||||
return recs, nil
|
||||
}
|
||||
|
||||
func (c *cloudflareProvider) deletePageRule(recordID, domainID string) error {
|
||||
endpoint := fmt.Sprintf(singlePageRuleURL, 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
|
||||
return c.cfClient.DeletePageRule(context.Background(), domainID, recordID)
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@ -467,117 +261,34 @@ func (c *cloudflareProvider) updatePageRule(recordID, domainID string, target st
|
||||
}
|
||||
|
||||
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
|
||||
parts := strings.Split(data, ",")
|
||||
parts := strings.Split(target, ",")
|
||||
priority, _ := strconv.Atoi(parts[2])
|
||||
code, _ := strconv.Atoi(parts[3])
|
||||
fwdInfo := &pageRuleFwdInfo{
|
||||
StatusCode: code,
|
||||
URL: parts[1],
|
||||
}
|
||||
dat, _ := json.Marshal(fwdInfo)
|
||||
pr := &pageRule{
|
||||
pr := cloudflare.PageRule{
|
||||
Status: "active",
|
||||
Priority: priority,
|
||||
Targets: []pageRuleTarget{
|
||||
Targets: []cloudflare.PageRuleTarget{
|
||||
{Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: parts[0]}},
|
||||
},
|
||||
Actions: []pageRuleAction{
|
||||
{ID: "forwarding_url", Value: json.RawMessage(dat)},
|
||||
Actions: []cloudflare.PageRuleAction{
|
||||
{ID: "forwarding_url", Value: &pageRuleFwdInfo{
|
||||
StatusCode: code,
|
||||
URL: parts[1],
|
||||
}},
|
||||
},
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
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))
|
||||
_, err := c.cfClient.CreatePageRule(context.Background(), domainID, pr)
|
||||
return err
|
||||
}
|
||||
|
||||
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 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"`
|
||||
}
|
||||
|
||||
// go-staticcheck lies!
|
||||
type pageRuleConstraint struct {
|
||||
Operator string `json:"operator"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type pageRuleAction struct {
|
||||
ID string `json:"id"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
}
|
||||
|
||||
type pageRuleFwdInfo struct {
|
||||
URL string `json:"url"`
|
||||
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"`
|
||||
}
|
||||
|
Reference in New Issue
Block a user