mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-01-13 10:58:17 +08:00
439 lines
13 KiB
Go
439 lines
13 KiB
Go
package cloudflare
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/StackExchange/dnscontrol/v3/models"
|
|
"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{}
|
|
zones, err := c.cfClient.ListZones(context.Background())
|
|
if err != nil {
|
|
return fmt.Errorf("failed fetching domain list from cloudflare(%q): %s", c.cfClient.APIEmail, 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) {
|
|
records := []*models.RecordConfig{}
|
|
rrs, err := c.cfClient.DNSRecords(context.Background(), id, cloudflare.DNSRecord{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed fetching record list from cloudflare(%q): %w", c.cfClient.APIEmail, err)
|
|
}
|
|
for _, rec := range rrs {
|
|
rt, err := c.nativeToRecord(domain, rec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, rt)
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
func (c *cloudflareProvider) deleteDNSRecord(rec cloudflare.DNSRecord, domainID string) error {
|
|
return c.cfClient.DeleteDNSRecord(context.Background(), domainID, rec.ID)
|
|
}
|
|
|
|
// create a correction to delete a record
|
|
func (c *cloudflareProvider) deleteRec(rec cloudflare.DNSRecord, domainID string) *models.Correction {
|
|
return &models.Correction{
|
|
Msg: fmt.Sprintf("DELETE record: %s %s %d %q (id=%s)", rec.Name, rec.Type, rec.TTL, rec.Content, rec.ID),
|
|
F: func() error {
|
|
err := c.cfClient.DeleteDNSRecord(context.Background(), domainID, rec.ID)
|
|
return err
|
|
},
|
|
}
|
|
}
|
|
|
|
func (c *cloudflareProvider) createZone(domainName string) (string, error) {
|
|
zone, err := c.cfClient.CreateZone(context.Background(), domainName, false, cloudflare.Account{ID: c.cfClient.AccountID}, "full")
|
|
return zone.ID, err
|
|
}
|
|
|
|
func cfDSData(rec *models.RecordConfig) *cfRecData {
|
|
return &cfRecData{
|
|
KeyTag: rec.DsKeyTag,
|
|
Algorithm: rec.DsAlgorithm,
|
|
DigestType: rec.DsDigestType,
|
|
Digest: rec.DsDigest,
|
|
}
|
|
}
|
|
|
|
func cfSrvData(rec *models.RecordConfig) *cfRecData {
|
|
serverParts := strings.Split(rec.GetLabelFQDN(), ".")
|
|
c := &cfRecData{
|
|
Service: serverParts[0],
|
|
Proto: serverParts[1],
|
|
Name: strings.Join(serverParts[2:], "."),
|
|
Port: rec.SrvPort,
|
|
Priority: rec.SrvPriority,
|
|
Weight: rec.SrvWeight,
|
|
}
|
|
c.Target = cfTarget(rec.GetTargetField())
|
|
return c
|
|
}
|
|
|
|
func cfCaaData(rec *models.RecordConfig) *cfRecData {
|
|
return &cfRecData{
|
|
Tag: rec.CaaTag,
|
|
Flags: rec.CaaFlag,
|
|
Value: rec.GetTargetField(),
|
|
}
|
|
}
|
|
|
|
func cfTlsaData(rec *models.RecordConfig) *cfRecData {
|
|
return &cfRecData{
|
|
Usage: rec.TlsaUsage,
|
|
Selector: rec.TlsaSelector,
|
|
MatchingType: rec.TlsaMatchingType,
|
|
Certificate: rec.GetTargetField(),
|
|
}
|
|
}
|
|
|
|
func cfSshfpData(rec *models.RecordConfig) *cfRecData {
|
|
return &cfRecData{
|
|
Algorithm: rec.SshfpAlgorithm,
|
|
HashType: rec.SshfpFingerprint,
|
|
Fingerprint: rec.GetTargetField(),
|
|
}
|
|
}
|
|
|
|
func (c *cloudflareProvider) createRec(rec *models.RecordConfig, domainID string) []*models.Correction {
|
|
var id string
|
|
content := rec.GetTargetField()
|
|
if rec.Metadata[metaOriginalIP] != "" {
|
|
content = rec.Metadata[metaOriginalIP]
|
|
}
|
|
prio := ""
|
|
if rec.Type == "MX" {
|
|
prio = fmt.Sprintf(" %d ", rec.MxPreference)
|
|
}
|
|
if rec.Type == "TXT" {
|
|
content = rec.GetTargetTXTJoined()
|
|
}
|
|
if rec.Type == "DS" {
|
|
content = fmt.Sprintf("%d %d %d %s", rec.DsKeyTag, rec.DsAlgorithm, rec.DsDigestType, rec.DsDigest)
|
|
}
|
|
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 := cloudflare.DNSRecord{
|
|
Name: rec.GetLabel(),
|
|
Type: rec.Type,
|
|
TTL: int(rec.TTL),
|
|
Content: content,
|
|
Priority: &rec.MxPreference,
|
|
}
|
|
if rec.Type == "SRV" {
|
|
cf.Data = cfSrvData(rec)
|
|
cf.Name = rec.GetLabelFQDN()
|
|
} else if rec.Type == "CAA" {
|
|
cf.Data = cfCaaData(rec)
|
|
cf.Name = rec.GetLabelFQDN()
|
|
cf.Content = ""
|
|
} else if rec.Type == "TLSA" {
|
|
cf.Data = cfTlsaData(rec)
|
|
cf.Name = rec.GetLabelFQDN()
|
|
} else if rec.Type == "SSHFP" {
|
|
cf.Data = cfSshfpData(rec)
|
|
cf.Name = rec.GetLabelFQDN()
|
|
} else if rec.Type == "DS" {
|
|
cf.Data = cfDSData(rec)
|
|
}
|
|
resp, err := c.cfClient.CreateDNSRecord(context.Background(), domainID, cf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Updating id (from the outer scope) by side-effect, required for updating proxy mode
|
|
id = resp.Result.ID
|
|
return nil
|
|
},
|
|
}}
|
|
if rec.Metadata[metaProxy] != "off" {
|
|
arr = append(arr, &models.Correction{
|
|
Msg: fmt.Sprintf("ACTIVATE PROXY for new record %s %s %d %s", rec.GetLabel(), rec.Type, rec.TTL, rec.GetTargetField()),
|
|
F: func() error { return c.modifyRecord(domainID, id, true, rec) },
|
|
})
|
|
}
|
|
return arr
|
|
}
|
|
|
|
func (c *cloudflareProvider) createRecDiff2(rec *models.RecordConfig, domainID string, msg string) []*models.Correction {
|
|
|
|
content := rec.GetTargetField()
|
|
if rec.Metadata[metaOriginalIP] != "" {
|
|
content = rec.Metadata[metaOriginalIP]
|
|
}
|
|
prio := ""
|
|
if rec.Type == "MX" {
|
|
prio = fmt.Sprintf(" %d ", rec.MxPreference)
|
|
}
|
|
if rec.Type == "TXT" {
|
|
content = rec.GetTargetTXTJoined()
|
|
}
|
|
if rec.Type == "DS" {
|
|
content = fmt.Sprintf("%d %d %d %s", rec.DsKeyTag, rec.DsAlgorithm, rec.DsDigestType, rec.DsDigest)
|
|
}
|
|
if msg == "" {
|
|
msg = fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.GetLabel(), rec.Type, rec.TTL, prio, content)
|
|
}
|
|
if rec.Metadata[metaProxy] == "on" {
|
|
msg = msg + fmt.Sprintf("\nACTIVATE PROXY for new record %s %s %d %s", rec.GetLabel(), rec.Type, rec.TTL, rec.GetTargetField())
|
|
}
|
|
arr := []*models.Correction{{
|
|
Msg: msg,
|
|
F: func() error {
|
|
cf := cloudflare.DNSRecord{
|
|
Name: rec.GetLabel(),
|
|
Type: rec.Type,
|
|
TTL: int(rec.TTL),
|
|
Content: content,
|
|
Priority: &rec.MxPreference,
|
|
}
|
|
if rec.Type == "SRV" {
|
|
cf.Data = cfSrvData(rec)
|
|
cf.Name = rec.GetLabelFQDN()
|
|
} else if rec.Type == "CAA" {
|
|
cf.Data = cfCaaData(rec)
|
|
cf.Name = rec.GetLabelFQDN()
|
|
cf.Content = ""
|
|
} else if rec.Type == "TLSA" {
|
|
cf.Data = cfTlsaData(rec)
|
|
cf.Name = rec.GetLabelFQDN()
|
|
} else if rec.Type == "SSHFP" {
|
|
cf.Data = cfSshfpData(rec)
|
|
cf.Name = rec.GetLabelFQDN()
|
|
} else if rec.Type == "DS" {
|
|
cf.Data = cfDSData(rec)
|
|
}
|
|
resp, err := c.cfClient.CreateDNSRecord(context.Background(), domainID, cf)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Records are created with the proxy off. If proxy should be
|
|
// enabled, we do a second API call.
|
|
resultID := resp.Result.ID
|
|
if rec.Metadata[metaProxy] == "on" {
|
|
return c.modifyRecord(domainID, resultID, true, rec)
|
|
}
|
|
return nil
|
|
},
|
|
}}
|
|
return arr
|
|
}
|
|
|
|
func (c *cloudflareProvider) modifyRecord(domainID, recID string, proxied bool, rec *models.RecordConfig) error {
|
|
if domainID == "" || recID == "" {
|
|
return fmt.Errorf("cannot modify record if domain or record id are empty")
|
|
}
|
|
|
|
r := cloudflare.DNSRecord{
|
|
ID: recID,
|
|
Proxied: &proxied,
|
|
Name: rec.GetLabel(),
|
|
Type: rec.Type,
|
|
Content: rec.GetTargetField(),
|
|
Priority: &rec.MxPreference,
|
|
TTL: int(rec.TTL),
|
|
}
|
|
if rec.Type == "TXT" {
|
|
r.Content = rec.GetTargetTXTJoined()
|
|
}
|
|
if rec.Type == "SRV" {
|
|
r.Data = cfSrvData(rec)
|
|
r.Name = rec.GetLabelFQDN()
|
|
} else if rec.Type == "CAA" {
|
|
r.Data = cfCaaData(rec)
|
|
r.Name = rec.GetLabelFQDN()
|
|
r.Content = ""
|
|
} else if rec.Type == "TLSA" {
|
|
r.Data = cfTlsaData(rec)
|
|
r.Name = rec.GetLabelFQDN()
|
|
} else if rec.Type == "SSHFP" {
|
|
r.Data = cfSshfpData(rec)
|
|
r.Name = rec.GetLabelFQDN()
|
|
} else if rec.Type == "DS" {
|
|
r.Data = cfDSData(rec)
|
|
r.Content = ""
|
|
}
|
|
return c.cfClient.UpdateDNSRecord(context.Background(), domainID, recID, r)
|
|
}
|
|
|
|
// change universal ssl state
|
|
func (c *cloudflareProvider) changeUniversalSSL(domainID string, state bool) error {
|
|
_, err := c.cfClient.EditUniversalSSLSetting(context.Background(), domainID, cloudflare.UniversalSSLSetting{Enabled: state})
|
|
return err
|
|
}
|
|
|
|
// get universal ssl state
|
|
func (c *cloudflareProvider) getUniversalSSL(domainID string) (bool, error) {
|
|
result, err := c.cfClient.UniversalSSLSettingDetails(context.Background(), domainID)
|
|
return result.Enabled, err
|
|
}
|
|
|
|
func (c *cloudflareProvider) getPageRules(id string, domain string) ([]*models.RecordConfig, error) {
|
|
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 rules {
|
|
// only interested in forwarding rules. Lets be very specific, and skip anything else
|
|
if len(pr.Actions) != 1 || len(pr.Targets) != 1 {
|
|
continue
|
|
}
|
|
if pr.Actions[0].ID != "forwarding_url" {
|
|
continue
|
|
}
|
|
value := pr.Actions[0].Value.(map[string]interface{})
|
|
var thisPr = pr
|
|
r := &models.RecordConfig{
|
|
Type: "PAGE_RULE",
|
|
Original: thisPr,
|
|
TTL: 1,
|
|
}
|
|
r.SetLabel("@", domain)
|
|
r.SetTarget(fmt.Sprintf("%s,%s,%d,%d", // $FROM,$TO,$PRIO,$CODE
|
|
pr.Targets[0].Constraint.Value,
|
|
value["url"],
|
|
pr.Priority,
|
|
intZero(value["status_code"])))
|
|
recs = append(recs, r)
|
|
}
|
|
return recs, nil
|
|
}
|
|
|
|
func (c *cloudflareProvider) deletePageRule(recordID, domainID string) error {
|
|
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
|
|
}
|
|
return c.createPageRule(domainID, target)
|
|
}
|
|
|
|
func (c *cloudflareProvider) createPageRule(domainID string, target string) error {
|
|
// from to priority code
|
|
parts := strings.Split(target, ",")
|
|
priority, _ := strconv.Atoi(parts[2])
|
|
code, _ := strconv.Atoi(parts[3])
|
|
pr := cloudflare.PageRule{
|
|
Status: "active",
|
|
Priority: priority,
|
|
Targets: []cloudflare.PageRuleTarget{
|
|
{Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: parts[0]}},
|
|
},
|
|
Actions: []cloudflare.PageRuleAction{
|
|
{ID: "forwarding_url", Value: &pageRuleFwdInfo{
|
|
StatusCode: code,
|
|
URL: parts[1],
|
|
}},
|
|
},
|
|
}
|
|
_, err := c.cfClient.CreatePageRule(context.Background(), domainID, pr)
|
|
return err
|
|
}
|
|
|
|
func (c *cloudflareProvider) getWorkerRoutes(id string, domain string) ([]*models.RecordConfig, error) {
|
|
res, err := c.cfClient.ListWorkerRoutes(context.Background(), id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed fetching worker route list cloudflare: %s", err)
|
|
}
|
|
|
|
recs := []*models.RecordConfig{}
|
|
for _, pr := range res.Routes {
|
|
var thisPr = pr
|
|
r := &models.RecordConfig{
|
|
Type: "WORKER_ROUTE",
|
|
Original: thisPr,
|
|
TTL: 1,
|
|
}
|
|
r.SetLabel("@", domain)
|
|
r.SetTarget(fmt.Sprintf("%s,%s", // $PATTERN,$SCRIPT
|
|
pr.Pattern,
|
|
pr.Script))
|
|
recs = append(recs, r)
|
|
}
|
|
return recs, nil
|
|
}
|
|
|
|
func (c *cloudflareProvider) deleteWorkerRoute(recordID, domainID string) error {
|
|
_, err := c.cfClient.DeleteWorkerRoute(context.Background(), domainID, recordID)
|
|
return err
|
|
}
|
|
|
|
func (c *cloudflareProvider) updateWorkerRoute(recordID, domainID string, target string) error {
|
|
// Causing Stack Overflow (!?)
|
|
// return c.updateWorkerRoute(recordID, domainID, target)
|
|
|
|
if err := c.deleteWorkerRoute(recordID, domainID); err != nil {
|
|
return err
|
|
}
|
|
return c.createWorkerRoute(domainID, target)
|
|
}
|
|
|
|
func (c *cloudflareProvider) createWorkerRoute(domainID string, target string) error {
|
|
// $PATTERN,$SCRIPT
|
|
parts := strings.Split(target, ",")
|
|
if len(parts) != 2 {
|
|
return fmt.Errorf("unexpected target: '%s' (expected: 'PATTERN,SCRIPT')", target)
|
|
}
|
|
wr := cloudflare.WorkerRoute{
|
|
Pattern: parts[0],
|
|
Script: parts[1],
|
|
}
|
|
|
|
_, err := c.cfClient.CreateWorkerRoute(context.Background(), domainID, wr)
|
|
return err
|
|
}
|
|
|
|
func (c *cloudflareProvider) createTestWorker(workerName string) error {
|
|
wrp := cloudflare.WorkerRequestParams{
|
|
ZoneID: "",
|
|
ScriptName: workerName,
|
|
}
|
|
|
|
script := cloudflare.WorkerScriptParams{
|
|
Script: `
|
|
addEventListener("fetch", (event) => {
|
|
event.respondWith(
|
|
new Response("Ok.", { status: 200 })
|
|
);
|
|
});`,
|
|
}
|
|
|
|
_, err := c.cfClient.UploadWorker(context.Background(), &wrp, &script)
|
|
return err
|
|
}
|
|
|
|
// https://github.com/dominikh/go-tools/issues/1137 which is a dup of
|
|
// https://github.com/dominikh/go-tools/issues/810
|
|
//
|
|
//lint:ignore U1000 false positive due to
|
|
type pageRuleConstraint struct {
|
|
Operator string `json:"operator"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type pageRuleFwdInfo struct {
|
|
URL string `json:"url"`
|
|
StatusCode int `json:"status_code"`
|
|
}
|