diff --git a/go.mod b/go.mod index 3b291d158..3659b7793 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 0ee56638d..571c894a3 100644 --- a/go.sum +++ b/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= diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 9e10f10fa..bd90f2247 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -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 diff --git a/providers/cloudflare/cloudflareProvider.go b/providers/cloudflare/cloudflareProvider.go index d3040821e..d5388c792 100644 --- a/providers/cloudflare/cloudflareProvider.go +++ b/providers/cloudflare/cloudflareProvider.go @@ -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" } diff --git a/providers/cloudflare/rest.go b/providers/cloudflare/rest.go index aa909482c..84f4b5910 100644 --- a/providers/cloudflare/rest.go +++ b/providers/cloudflare/rest.go @@ -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"` -}