github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/cloudflare/cloudflareProvider.go (about) 1 package cloudflare 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "log" 7 "net" 8 "strings" 9 "time" 10 11 "github.com/miekg/dns/dnsutil" 12 13 "github.com/StackExchange/dnscontrol/v2/models" 14 "github.com/StackExchange/dnscontrol/v2/pkg/printer" 15 "github.com/StackExchange/dnscontrol/v2/pkg/transform" 16 "github.com/StackExchange/dnscontrol/v2/providers" 17 "github.com/StackExchange/dnscontrol/v2/providers/diff" 18 ) 19 20 /* 21 22 Cloudflare API DNS provider: 23 24 Info required in `creds.json`: 25 - apikey 26 - apiuser 27 - accountid (optional) 28 - accountname (optional) 29 30 Record level metadata available: 31 - cloudflare_proxy ("on", "off", or "full") 32 33 Domain level metadata available: 34 - cloudflare_proxy_default ("on", "off", or "full") 35 36 Provider level metadata available: 37 - ip_conversions 38 */ 39 40 var features = providers.DocumentationNotes{ 41 providers.CanUseAlias: providers.Can("CF automatically flattens CNAME records into A records dynamically"), 42 providers.CanUsePTR: providers.Cannot(), 43 providers.CanUseCAA: providers.Can(), 44 providers.CanUseSRV: providers.Can(), 45 providers.CanUseTLSA: providers.Can(), 46 providers.CanUseSSHFP: providers.Can(), 47 providers.DocCreateDomains: providers.Can(), 48 providers.DocDualHost: providers.Cannot("Cloudflare will not work well in situations where it is not the only DNS server"), 49 providers.DocOfficiallySupported: providers.Can(), 50 providers.CanGetZones: providers.Can(), 51 } 52 53 func init() { 54 providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", newCloudflare, features) 55 providers.RegisterCustomRecordType("CF_REDIRECT", "CLOUDFLAREAPI", "") 56 providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", "CLOUDFLAREAPI", "") 57 } 58 59 // CloudflareApi is the handle for API calls. 60 type CloudflareApi struct { 61 ApiKey string `json:"apikey"` 62 ApiToken string `json:"apitoken"` 63 ApiUser string `json:"apiuser"` 64 AccountID string `json:"accountid"` 65 AccountName string `json:"accountname"` 66 domainIndex map[string]string 67 nameservers map[string][]string 68 ipConversions []transform.IpConversion 69 ignoredLabels []string 70 manageRedirects bool 71 } 72 73 func labelMatches(label string, matches []string) bool { 74 printer.Debugf("DEBUG: labelMatches(%#v, %#v)\n", label, matches) 75 for _, tst := range matches { 76 if label == tst { 77 return true 78 } 79 } 80 return false 81 } 82 83 // GetNameservers returns the nameservers for a domain. 84 func (c *CloudflareApi) GetNameservers(domain string) ([]*models.Nameserver, error) { 85 if c.domainIndex == nil { 86 if err := c.fetchDomainList(); err != nil { 87 return nil, err 88 } 89 } 90 ns, ok := c.nameservers[domain] 91 if !ok { 92 return nil, fmt.Errorf("Nameservers for %s not found in cloudflare account", domain) 93 } 94 return models.StringsToNameservers(ns), nil 95 } 96 97 func (c *CloudflareApi) ListZones() ([]string, error) { 98 if err := c.fetchDomainList(); err != nil { 99 return nil, err 100 } 101 zones := make([]string, 0, len(c.domainIndex)) 102 for d := range c.domainIndex { 103 zones = append(zones, d) 104 } 105 return zones, nil 106 } 107 108 // GetZoneRecords gets the records of a zone and returns them in RecordConfig format. 109 func (c *CloudflareApi) GetZoneRecords(domain string) (models.Records, error) { 110 id, err := c.getDomainID(domain) 111 if err != nil { 112 return nil, err 113 } 114 records, err := c.getRecordsForDomain(id, domain) 115 if err != nil { 116 return nil, err 117 } 118 for _, rec := range records { 119 if rec.TTL == 1 { 120 rec.TTL = 0 121 } 122 } 123 return records, nil 124 } 125 126 func (c *CloudflareApi) getDomainID(name string) (string, error) { 127 if c.domainIndex == nil { 128 if err := c.fetchDomainList(); err != nil { 129 return "", err 130 } 131 } 132 id, ok := c.domainIndex[name] 133 if !ok { 134 return "", fmt.Errorf("'%s' not a zone in cloudflare account", name) 135 } 136 return id, nil 137 } 138 139 // GetDomainCorrections returns a list of corrections to update a domain. 140 func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 141 id, err := c.getDomainID(dc.Name) 142 if err != nil { 143 return nil, err 144 } 145 records, err := c.getRecordsForDomain(id, dc.Name) 146 if err != nil { 147 return nil, err 148 } 149 150 if err := c.preprocessConfig(dc); err != nil { 151 return nil, err 152 } 153 for i := len(records) - 1; i >= 0; i-- { 154 rec := records[i] 155 // Delete ignore labels 156 if labelMatches(dnsutil.TrimDomainName(rec.Original.(*cfRecord).Name, dc.Name), c.ignoredLabels) { 157 printer.Debugf("ignored_label: %s\n", rec.Original.(*cfRecord).Name) 158 records = append(records[:i], records[i+1:]...) 159 } 160 } 161 162 if c.manageRedirects { 163 prs, err := c.getPageRules(id, dc.Name) 164 if err != nil { 165 return nil, err 166 } 167 records = append(records, prs...) 168 } 169 170 for _, rec := range dc.Records { 171 if rec.Type == "ALIAS" { 172 rec.Type = "CNAME" 173 } 174 // As per CF-API documentation proxied records are always forced to have a TTL of 1. 175 // When not forcing this property change here, dnscontrol tries each time to update 176 // the TTL of a record which simply cannot be changed anyway. 177 if rec.Metadata[metaProxy] != "off" { 178 rec.TTL = 1 179 } 180 if labelMatches(rec.GetLabel(), c.ignoredLabels) { 181 log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.GetLabel(), c.ignoredLabels) 182 } 183 } 184 185 checkNSModifications(dc) 186 187 // Normalize 188 models.PostProcessRecords(records) 189 190 differ := diff.New(dc, getProxyMetadata) 191 _, create, del, mod := differ.IncrementalDiff(records) 192 corrections := []*models.Correction{} 193 194 for _, d := range del { 195 ex := d.Existing 196 if ex.Type == "PAGE_RULE" { 197 corrections = append(corrections, &models.Correction{ 198 Msg: d.String(), 199 F: func() error { return c.deletePageRule(ex.Original.(*pageRule).ID, id) }, 200 }) 201 202 } else { 203 corrections = append(corrections, c.deleteRec(ex.Original.(*cfRecord), id)) 204 } 205 } 206 for _, d := range create { 207 des := d.Desired 208 if des.Type == "PAGE_RULE" { 209 corrections = append(corrections, &models.Correction{ 210 Msg: d.String(), 211 F: func() error { return c.createPageRule(id, des.GetTargetField()) }, 212 }) 213 } else { 214 corrections = append(corrections, c.createRec(des, id)...) 215 } 216 } 217 218 for _, d := range mod { 219 rec := d.Desired 220 ex := d.Existing 221 if rec.Type == "PAGE_RULE" { 222 corrections = append(corrections, &models.Correction{ 223 Msg: d.String(), 224 F: func() error { return c.updatePageRule(ex.Original.(*pageRule).ID, id, rec.GetTargetField()) }, 225 }) 226 } else { 227 e := ex.Original.(*cfRecord) 228 proxy := e.Proxiable && rec.Metadata[metaProxy] != "off" 229 corrections = append(corrections, &models.Correction{ 230 Msg: d.String(), 231 F: func() error { return c.modifyRecord(id, e.ID, proxy, rec) }, 232 }) 233 } 234 } 235 236 // Add universalSSL change to corrections when needed 237 if changed, newState, err := c.checkUniversalSSL(dc, id); err == nil && changed { 238 var newStateString string 239 if newState { 240 newStateString = "enabled" 241 } else { 242 newStateString = "disabled" 243 } 244 corrections = append(corrections, &models.Correction{ 245 Msg: fmt.Sprintf("Universal SSL will be %s for this domain.", newStateString), 246 F: func() error { return c.changeUniversalSSL(id, newState) }, 247 }) 248 } 249 250 return corrections, nil 251 } 252 253 func checkNSModifications(dc *models.DomainConfig) { 254 newList := make([]*models.RecordConfig, 0, len(dc.Records)) 255 for _, rec := range dc.Records { 256 if rec.Type == "NS" && rec.GetLabelFQDN() == dc.Name { 257 if !strings.HasSuffix(rec.GetTargetField(), ".ns.cloudflare.com.") { 258 printer.Warnf("cloudflare does not support modifying NS records on base domain. %s will not be added.\n", rec.GetTargetField()) 259 } 260 continue 261 } 262 newList = append(newList, rec) 263 } 264 dc.Records = newList 265 } 266 267 func (c *CloudflareApi) checkUniversalSSL(dc *models.DomainConfig, id string) (changed bool, newState bool, err error) { 268 expected_str := dc.Metadata[metaUniversalSSL] 269 if expected_str == "" { 270 return false, false, fmt.Errorf("Metadata not set.") 271 } 272 273 if actual, err := c.getUniversalSSL(id); err == nil { 274 // convert str to bool 275 var expected bool 276 if expected_str == "off" { 277 expected = false 278 } else { 279 expected = true 280 } 281 // did something change? 282 if actual != expected { 283 return true, expected, nil 284 } 285 return false, expected, nil 286 } 287 return false, false, fmt.Errorf("error receiving universal ssl state:") 288 } 289 290 const ( 291 metaProxy = "cloudflare_proxy" 292 metaProxyDefault = metaProxy + "_default" 293 metaOriginalIP = "original_ip" // TODO(tlim): Unclear what this means. 294 metaUniversalSSL = "cloudflare_universalssl" 295 metaIPConversions = "ip_conversions" // TODO(tlim): Rename to obscure_rules. 296 ) 297 298 func checkProxyVal(v string) (string, error) { 299 v = strings.ToLower(v) 300 if v != "on" && v != "off" && v != "full" { 301 return "", fmt.Errorf("Bad metadata value for cloudflare_proxy: '%s'. Use on/off/full", v) 302 } 303 return v, nil 304 } 305 306 func (c *CloudflareApi) preprocessConfig(dc *models.DomainConfig) error { 307 308 // Determine the default proxy setting. 309 var defProxy string 310 var err error 311 if defProxy = dc.Metadata[metaProxyDefault]; defProxy == "" { 312 defProxy = "off" 313 } else { 314 defProxy, err = checkProxyVal(defProxy) 315 if err != nil { 316 return err 317 } 318 } 319 320 // Check UniversalSSL setting 321 if u := dc.Metadata[metaUniversalSSL]; u != "" { 322 u = strings.ToLower(u) 323 if u != "on" && u != "off" { 324 return fmt.Errorf("Bad metadata value for %s: '%s'. Use on/off.", metaUniversalSSL, u) 325 } 326 } 327 328 // Normalize the proxy setting for each record. 329 // A and CNAMEs: Validate. If null, set to default. 330 // else: Make sure it wasn't set. Set to default. 331 // iterate backwards so first defined page rules have highest priority 332 currentPrPrio := 1 333 for i := len(dc.Records) - 1; i >= 0; i-- { 334 rec := dc.Records[i] 335 if rec.Metadata == nil { 336 rec.Metadata = map[string]string{} 337 } 338 // cloudflare uses "1" to mean "auto-ttl" 339 // if we get here and ttl is not specified (or is the dnscontrol default of 300), 340 // use automatic mode instead. 341 if rec.TTL == 0 || rec.TTL == 300 { 342 rec.TTL = 1 343 } 344 if rec.TTL != 1 && rec.TTL < 120 { 345 rec.TTL = 120 346 } 347 348 if rec.Type != "A" && rec.Type != "CNAME" && rec.Type != "AAAA" && rec.Type != "ALIAS" { 349 if rec.Metadata[metaProxy] != "" { 350 return fmt.Errorf("cloudflare_proxy set on %v record: %#v cloudflare_proxy=%#v", rec.Type, rec.GetLabel(), rec.Metadata[metaProxy]) 351 } 352 // Force it to off. 353 rec.Metadata[metaProxy] = "off" 354 } else { 355 if val := rec.Metadata[metaProxy]; val == "" { 356 rec.Metadata[metaProxy] = defProxy 357 } else { 358 val, err := checkProxyVal(val) 359 if err != nil { 360 return err 361 } 362 rec.Metadata[metaProxy] = val 363 } 364 } 365 366 // CF_REDIRECT record types. Encode target as $FROM,$TO,$PRIO,$CODE 367 if rec.Type == "CF_REDIRECT" || rec.Type == "CF_TEMP_REDIRECT" { 368 if !c.manageRedirects { 369 return fmt.Errorf("you must add 'manage_redirects: true' metadata to cloudflare provider to use CF_REDIRECT records") 370 } 371 parts := strings.Split(rec.GetTargetField(), ",") 372 if len(parts) != 2 { 373 return fmt.Errorf("Invalid data specified for cloudflare redirect record") 374 } 375 code := 301 376 if rec.Type == "CF_TEMP_REDIRECT" { 377 code = 302 378 } 379 rec.SetTarget(fmt.Sprintf("%s,%d,%d", rec.GetTargetField(), currentPrPrio, code)) 380 currentPrPrio++ 381 rec.Type = "PAGE_RULE" 382 } 383 } 384 385 // look for ip conversions and transform records 386 for _, rec := range dc.Records { 387 if rec.Type != "A" { 388 continue 389 } 390 // only transform "full" 391 if rec.Metadata[metaProxy] != "full" { 392 continue 393 } 394 ip := net.ParseIP(rec.GetTargetField()) 395 if ip == nil { 396 return fmt.Errorf("%s is not a valid ip address", rec.GetTargetField()) 397 } 398 newIP, err := transform.TransformIP(ip, c.ipConversions) 399 if err != nil { 400 return err 401 } 402 rec.Metadata[metaOriginalIP] = rec.GetTargetField() 403 rec.SetTarget(newIP.String()) 404 } 405 406 return nil 407 } 408 409 func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 410 api := &CloudflareApi{} 411 api.ApiUser, api.ApiKey, api.ApiToken = m["apiuser"], m["apikey"], m["apitoken"] 412 // check api keys from creds json file 413 if api.ApiToken == "" && (api.ApiKey == "" || api.ApiUser == "") { 414 return nil, fmt.Errorf("if cloudflare apitoken is not set, apikey and apiuser must be provided") 415 } 416 if api.ApiToken != "" && (api.ApiKey != "" || api.ApiUser != "") { 417 return nil, fmt.Errorf("if cloudflare apitoken is set, apikey and apiuser should not be provided") 418 } 419 420 // Check account data if set 421 api.AccountID, api.AccountName = m["accountid"], m["accountname"] 422 if (api.AccountID != "" && api.AccountName == "") || (api.AccountID == "" && api.AccountName != "") { 423 return nil, fmt.Errorf("either both cloudflare accountid and accountname must be provided or neither") 424 } 425 426 err := api.fetchDomainList() 427 if err != nil { 428 return nil, err 429 } 430 431 if len(metadata) > 0 { 432 parsedMeta := &struct { 433 IPConversions string `json:"ip_conversions"` 434 IgnoredLabels []string `json:"ignored_labels"` 435 ManageRedirects bool `json:"manage_redirects"` 436 }{} 437 err := json.Unmarshal([]byte(metadata), parsedMeta) 438 if err != nil { 439 return nil, err 440 } 441 api.manageRedirects = parsedMeta.ManageRedirects 442 // ignored_labels: 443 for _, l := range parsedMeta.IgnoredLabels { 444 api.ignoredLabels = append(api.ignoredLabels, l) 445 } 446 if len(api.ignoredLabels) > 0 { 447 printer.Warnf("Cloudflare 'ignored_labels' configuration is deprecated and might be removed. Please use the IGNORE domain directive to achieve the same effect.\n") 448 } 449 // parse provider level metadata 450 if len(parsedMeta.IPConversions) > 0 { 451 api.ipConversions, err = transform.DecodeTransformTable(parsedMeta.IPConversions) 452 if err != nil { 453 return nil, err 454 } 455 } 456 } 457 return api, nil 458 } 459 460 // Used on the "existing" records. 461 type cfRecData struct { 462 Name string `json:"name"` 463 Target cfTarget `json:"target"` 464 Service string `json:"service"` // SRV 465 Proto string `json:"proto"` // SRV 466 Priority uint16 `json:"priority"` // SRV 467 Weight uint16 `json:"weight"` // SRV 468 Port uint16 `json:"port"` // SRV 469 Tag string `json:"tag"` // CAA 470 Flags uint8 `json:"flags"` // CAA 471 Value string `json:"value"` // CAA 472 Usage uint8 `json:"usage"` // TLSA 473 Selector uint8 `json:"selector"` // TLSA 474 Matching_Type uint8 `json:"matching_type"` // TLSA 475 Certificate string `json:"certificate"` // TLSA 476 Algorithm uint8 `json:"algorithm"` // SSHFP 477 Hash_Type uint8 `json:"type"` // SSHFP 478 Fingerprint string `json:"fingerprint"` // SSHFP 479 } 480 481 // cfTarget is a SRV target. A null target is represented by an empty string, but 482 // a dot is so acceptable. 483 type cfTarget string 484 485 // UnmarshalJSON decodes a SRV target from the Cloudflare API. A null target is 486 // represented by a false boolean or a dot. Domain names are FQDNs without a 487 // trailing period (as of 2019-11-05). 488 func (c *cfTarget) UnmarshalJSON(data []byte) error { 489 var obj interface{} 490 if err := json.Unmarshal(data, &obj); err != nil { 491 return err 492 } 493 switch v := obj.(type) { 494 case string: 495 *c = cfTarget(v) 496 case bool: 497 if v { 498 panic("unknown value for cfTarget bool: true") 499 } 500 *c = "" // the "." is already added by nativeToRecord 501 } 502 return nil 503 } 504 505 // MarshalJSON encodes cfTarget for the Cloudflare API. Null targets are 506 // represented by a single period. 507 func (c cfTarget) MarshalJSON() ([]byte, error) { 508 var obj string 509 switch c { 510 case "", ".": 511 obj = "." 512 default: 513 obj = string(c) 514 } 515 return json.Marshal(obj) 516 } 517 518 // DNSControlString returns cfTarget normalized to be a FQDN. Null targets are 519 // represented by a single period. 520 func (c cfTarget) FQDN() string { 521 return strings.TrimRight(string(c), ".") + "." 522 } 523 524 type cfRecord struct { 525 ID string `json:"id"` 526 Type string `json:"type"` 527 Name string `json:"name"` 528 Content string `json:"content"` 529 Proxiable bool `json:"proxiable"` 530 Proxied bool `json:"proxied"` 531 TTL uint32 `json:"ttl"` 532 Locked bool `json:"locked"` 533 ZoneID string `json:"zone_id"` 534 ZoneName string `json:"zone_name"` 535 CreatedOn time.Time `json:"created_on"` 536 ModifiedOn time.Time `json:"modified_on"` 537 Data *cfRecData `json:"data"` 538 Priority json.Number `json:"priority"` 539 } 540 541 func (c *cfRecord) nativeToRecord(domain string) *models.RecordConfig { 542 // normalize cname,mx,ns records with dots to be consistent with our config format. 543 if c.Type == "CNAME" || c.Type == "MX" || c.Type == "NS" || c.Type == "SRV" { 544 c.Content = dnsutil.AddOrigin(c.Content+".", domain) 545 } 546 547 rc := &models.RecordConfig{ 548 TTL: c.TTL, 549 Original: c, 550 } 551 rc.SetLabelFromFQDN(c.Name, domain) 552 553 // workaround for https://github.com/StackExchange/dnscontrol/issues/446 554 if c.Type == "SPF" { 555 c.Type = "TXT" 556 } 557 558 switch rType := c.Type; rType { // #rtype_variations 559 case "MX": 560 var priority uint16 561 if c.Priority == "" { 562 priority = 0 563 } else { 564 if p, err := c.Priority.Int64(); err != nil { 565 panic(fmt.Errorf("error decoding priority from cloudflare record: %w", err)) 566 } else { 567 priority = uint16(p) 568 } 569 } 570 if err := rc.SetTargetMX(priority, c.Content); err != nil { 571 panic(fmt.Errorf("unparsable MX record received from cloudflare: %w", err)) 572 } 573 case "SRV": 574 data := *c.Data 575 if err := rc.SetTargetSRV(data.Priority, data.Weight, data.Port, 576 dnsutil.AddOrigin(data.Target.FQDN(), domain)); err != nil { 577 panic(fmt.Errorf("unparsable SRV record received from cloudflare: %w", err)) 578 } 579 default: // "A", "AAAA", "ANAME", "CAA", "CNAME", "NS", "PTR", "TXT" 580 if err := rc.PopulateFromString(rType, c.Content, domain); err != nil { 581 panic(fmt.Errorf("unparsable record received from cloudflare: %w", err)) 582 } 583 } 584 585 return rc 586 } 587 588 func getProxyMetadata(r *models.RecordConfig) map[string]string { 589 if r.Type != "A" && r.Type != "AAAA" && r.Type != "CNAME" { 590 return nil 591 } 592 proxied := false 593 if r.Original != nil { 594 proxied = r.Original.(*cfRecord).Proxied 595 } else { 596 proxied = r.Metadata[metaProxy] != "off" 597 } 598 return map[string]string{ 599 "proxy": fmt.Sprint(proxied), 600 } 601 } 602 603 // EnsureDomainExists returns an error of domain does not exist. 604 func (c *CloudflareApi) EnsureDomainExists(domain string) error { 605 if _, ok := c.domainIndex[domain]; ok { 606 return nil 607 } 608 var id string 609 id, err := c.createZone(domain) 610 fmt.Printf("Added zone for %s to Cloudflare account: %s\n", domain, id) 611 return err 612 }