github.com/philhug/dnscontrol@v0.2.4-0.20180625181521-921fa9849001/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/StackExchange/dnscontrol/models" 12 "github.com/StackExchange/dnscontrol/pkg/transform" 13 "github.com/StackExchange/dnscontrol/providers" 14 "github.com/StackExchange/dnscontrol/providers/diff" 15 "github.com/miekg/dns/dnsutil" 16 "github.com/pkg/errors" 17 ) 18 19 /* 20 21 Cloudflare API DNS provider: 22 23 Info required in `creds.json`: 24 - apikey 25 - apiuser 26 27 Record level metadata available: 28 - cloudflare_proxy ("on", "off", or "full") 29 30 Domain level metadata available: 31 - cloudflare_proxy_default ("on", "off", or "full") 32 33 Provider level metadata available: 34 - ip_conversions 35 */ 36 37 var features = providers.DocumentationNotes{ 38 providers.CanUseAlias: providers.Can("CF automatically flattens CNAME records into A records dynamically"), 39 providers.CanUseCAA: providers.Can(), 40 providers.CanUseSRV: providers.Can(), 41 providers.DocCreateDomains: providers.Can(), 42 providers.DocDualHost: providers.Cannot("Cloudflare will not work well in situations where it is not the only DNS server"), 43 providers.DocOfficiallySupported: providers.Can(), 44 } 45 46 func init() { 47 providers.RegisterDomainServiceProviderType("CLOUDFLAREAPI", newCloudflare, features) 48 providers.RegisterCustomRecordType("CF_REDIRECT", "CLOUDFLAREAPI", "") 49 providers.RegisterCustomRecordType("CF_TEMP_REDIRECT", "CLOUDFLAREAPI", "") 50 } 51 52 // CloudflareApi is the handle for API calls. 53 type CloudflareApi struct { 54 ApiKey string `json:"apikey"` 55 ApiUser string `json:"apiuser"` 56 domainIndex map[string]string 57 nameservers map[string][]string 58 ipConversions []transform.IpConversion 59 ignoredLabels []string 60 manageRedirects bool 61 } 62 63 func labelMatches(label string, matches []string) bool { 64 // log.Printf("DEBUG: labelMatches(%#v, %#v)\n", label, matches) 65 for _, tst := range matches { 66 if label == tst { 67 return true 68 } 69 } 70 return false 71 } 72 73 // GetNameservers returns the nameservers for a domain. 74 func (c *CloudflareApi) GetNameservers(domain string) ([]*models.Nameserver, error) { 75 if c.domainIndex == nil { 76 if err := c.fetchDomainList(); err != nil { 77 return nil, err 78 } 79 } 80 ns, ok := c.nameservers[domain] 81 if !ok { 82 return nil, errors.Errorf("Nameservers for %s not found in cloudflare account", domain) 83 } 84 return models.StringsToNameservers(ns), nil 85 } 86 87 // GetDomainCorrections returns a list of corrections to update a domain. 88 func (c *CloudflareApi) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) { 89 if c.domainIndex == nil { 90 if err := c.fetchDomainList(); err != nil { 91 return nil, err 92 } 93 } 94 id, ok := c.domainIndex[dc.Name] 95 if !ok { 96 return nil, errors.Errorf("%s not listed in zones for cloudflare account", dc.Name) 97 } 98 if err := c.preprocessConfig(dc); err != nil { 99 return nil, err 100 } 101 records, err := c.getRecordsForDomain(id, dc.Name) 102 if err != nil { 103 return nil, err 104 } 105 for i := len(records) - 1; i >= 0; i-- { 106 rec := records[i] 107 // Delete ignore labels 108 if labelMatches(dnsutil.TrimDomainName(rec.Original.(*cfRecord).Name, dc.Name), c.ignoredLabels) { 109 fmt.Printf("ignored_label: %s\n", rec.Original.(*cfRecord).Name) 110 records = append(records[:i], records[i+1:]...) 111 } 112 } 113 if c.manageRedirects { 114 prs, err := c.getPageRules(id, dc.Name) 115 if err != nil { 116 return nil, err 117 } 118 records = append(records, prs...) 119 } 120 for _, rec := range dc.Records { 121 if rec.Type == "ALIAS" { 122 rec.Type = "CNAME" 123 } 124 if labelMatches(rec.GetLabel(), c.ignoredLabels) { 125 log.Fatalf("FATAL: dnsconfig contains label that matches ignored_labels: %#v is in %v)\n", rec.GetLabel(), c.ignoredLabels) 126 } 127 } 128 checkNSModifications(dc) 129 130 // Normalize 131 models.PostProcessRecords(records) 132 133 differ := diff.New(dc, getProxyMetadata) 134 _, create, del, mod := differ.IncrementalDiff(records) 135 corrections := []*models.Correction{} 136 137 for _, d := range del { 138 ex := d.Existing 139 if ex.Type == "PAGE_RULE" { 140 corrections = append(corrections, &models.Correction{ 141 Msg: d.String(), 142 F: func() error { return c.deletePageRule(ex.Original.(*pageRule).ID, id) }, 143 }) 144 145 } else { 146 corrections = append(corrections, c.deleteRec(ex.Original.(*cfRecord), id)) 147 } 148 } 149 for _, d := range create { 150 des := d.Desired 151 if des.Type == "PAGE_RULE" { 152 corrections = append(corrections, &models.Correction{ 153 Msg: d.String(), 154 F: func() error { return c.createPageRule(id, des.GetTargetField()) }, 155 }) 156 } else { 157 corrections = append(corrections, c.createRec(des, id)...) 158 } 159 } 160 161 for _, d := range mod { 162 rec := d.Desired 163 ex := d.Existing 164 if rec.Type == "PAGE_RULE" { 165 corrections = append(corrections, &models.Correction{ 166 Msg: d.String(), 167 F: func() error { return c.updatePageRule(ex.Original.(*pageRule).ID, id, rec.GetTargetField()) }, 168 }) 169 } else { 170 e := ex.Original.(*cfRecord) 171 proxy := e.Proxiable && rec.Metadata[metaProxy] != "off" 172 corrections = append(corrections, &models.Correction{ 173 Msg: d.String(), 174 F: func() error { return c.modifyRecord(id, e.ID, proxy, rec) }, 175 }) 176 } 177 } 178 return corrections, nil 179 } 180 181 func checkNSModifications(dc *models.DomainConfig) { 182 newList := make([]*models.RecordConfig, 0, len(dc.Records)) 183 for _, rec := range dc.Records { 184 if rec.Type == "NS" && rec.GetLabelFQDN() == dc.Name { 185 if !strings.HasSuffix(rec.GetTargetField(), ".ns.cloudflare.com.") { 186 log.Printf("Warning: cloudflare does not support modifying NS records on base domain. %s will not be added.", rec.GetTargetField()) 187 } 188 continue 189 } 190 newList = append(newList, rec) 191 } 192 dc.Records = newList 193 } 194 195 const ( 196 metaProxy = "cloudflare_proxy" 197 metaProxyDefault = metaProxy + "_default" 198 metaOriginalIP = "original_ip" // TODO(tlim): Unclear what this means. 199 metaIPConversions = "ip_conversions" // TODO(tlim): Rename to obscure_rules. 200 ) 201 202 func checkProxyVal(v string) (string, error) { 203 v = strings.ToLower(v) 204 if v != "on" && v != "off" && v != "full" { 205 return "", errors.Errorf("Bad metadata value for cloudflare_proxy: '%s'. Use on/off/full", v) 206 } 207 return v, nil 208 } 209 210 func (c *CloudflareApi) preprocessConfig(dc *models.DomainConfig) error { 211 212 // Determine the default proxy setting. 213 var defProxy string 214 var err error 215 if defProxy = dc.Metadata[metaProxyDefault]; defProxy == "" { 216 defProxy = "off" 217 } else { 218 defProxy, err = checkProxyVal(defProxy) 219 if err != nil { 220 return err 221 } 222 } 223 224 currentPrPrio := 1 225 226 // Normalize the proxy setting for each record. 227 // A and CNAMEs: Validate. If null, set to default. 228 // else: Make sure it wasn't set. Set to default. 229 // iterate backwards so first defined page rules have highest priority 230 for i := len(dc.Records) - 1; i >= 0; i-- { 231 rec := dc.Records[i] 232 if rec.Metadata == nil { 233 rec.Metadata = map[string]string{} 234 } 235 if rec.TTL == 0 || rec.TTL == 300 { 236 rec.TTL = 1 237 } 238 if rec.TTL != 1 && rec.TTL < 120 { 239 rec.TTL = 120 240 } 241 if rec.Type != "A" && rec.Type != "CNAME" && rec.Type != "AAAA" && rec.Type != "ALIAS" { 242 if rec.Metadata[metaProxy] != "" { 243 return errors.Errorf("cloudflare_proxy set on %v record: %#v cloudflare_proxy=%#v", rec.Type, rec.GetLabel(), rec.Metadata[metaProxy]) 244 } 245 // Force it to off. 246 rec.Metadata[metaProxy] = "off" 247 } else { 248 if val := rec.Metadata[metaProxy]; val == "" { 249 rec.Metadata[metaProxy] = defProxy 250 } else { 251 val, err := checkProxyVal(val) 252 if err != nil { 253 return err 254 } 255 rec.Metadata[metaProxy] = val 256 } 257 } 258 // CF_REDIRECT record types. Encode target as $FROM,$TO,$PRIO,$CODE 259 if rec.Type == "CF_REDIRECT" || rec.Type == "CF_TEMP_REDIRECT" { 260 if !c.manageRedirects { 261 return errors.Errorf("you must add 'manage_redirects: true' metadata to cloudflare provider to use CF_REDIRECT records") 262 } 263 parts := strings.Split(rec.GetTargetField(), ",") 264 if len(parts) != 2 { 265 return errors.Errorf("Invalid data specified for cloudflare redirect record") 266 } 267 code := 301 268 if rec.Type == "CF_TEMP_REDIRECT" { 269 code = 302 270 } 271 rec.SetTarget(fmt.Sprintf("%s,%d,%d", rec.GetTargetField(), currentPrPrio, code)) 272 currentPrPrio++ 273 rec.Type = "PAGE_RULE" 274 } 275 } 276 277 // look for ip conversions and transform records 278 for _, rec := range dc.Records { 279 if rec.Type != "A" { 280 continue 281 } 282 // only transform "full" 283 if rec.Metadata[metaProxy] != "full" { 284 continue 285 } 286 ip := net.ParseIP(rec.GetTargetField()) 287 if ip == nil { 288 return errors.Errorf("%s is not a valid ip address", rec.GetTargetField()) 289 } 290 newIP, err := transform.TransformIP(ip, c.ipConversions) 291 if err != nil { 292 return err 293 } 294 rec.Metadata[metaOriginalIP] = rec.GetTargetField() 295 rec.SetTarget(newIP.String()) 296 } 297 298 return nil 299 } 300 301 func newCloudflare(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { 302 api := &CloudflareApi{} 303 api.ApiUser, api.ApiKey = m["apiuser"], m["apikey"] 304 // check api keys from creds json file 305 if api.ApiKey == "" || api.ApiUser == "" { 306 return nil, errors.Errorf("cloudflare apikey and apiuser must be provided") 307 } 308 309 err := api.fetchDomainList() 310 if err != nil { 311 return nil, err 312 } 313 314 if len(metadata) > 0 { 315 parsedMeta := &struct { 316 IPConversions string `json:"ip_conversions"` 317 IgnoredLabels []string `json:"ignored_labels"` 318 ManageRedirects bool `json:"manage_redirects"` 319 }{} 320 err := json.Unmarshal([]byte(metadata), parsedMeta) 321 if err != nil { 322 return nil, err 323 } 324 api.manageRedirects = parsedMeta.ManageRedirects 325 // ignored_labels: 326 for _, l := range parsedMeta.IgnoredLabels { 327 api.ignoredLabels = append(api.ignoredLabels, l) 328 } 329 if len(api.ignoredLabels) > 0 { 330 log.Println("Warning: Cloudflare 'ignored_labels' configuration is deprecated and might be removed. Please use the IGNORE domain directive to achieve the same effect.") 331 } 332 // parse provider level metadata 333 if len(parsedMeta.IPConversions) > 0 { 334 api.ipConversions, err = transform.DecodeTransformTable(parsedMeta.IPConversions) 335 if err != nil { 336 return nil, err 337 } 338 } 339 } 340 return api, nil 341 } 342 343 // Used on the "existing" records. 344 type cfRecData struct { 345 Name string `json:"name"` 346 Target string `json:"target"` 347 Service string `json:"service"` // SRV 348 Proto string `json:"proto"` // SRV 349 Priority uint16 `json:"priority"` // SRV 350 Weight uint16 `json:"weight"` // SRV 351 Port uint16 `json:"port"` // SRV 352 Tag string `json:"tag"` // CAA 353 Flags uint8 `json:"flags"` // CAA 354 Value string `json:"value"` // CAA 355 } 356 357 type cfRecord struct { 358 ID string `json:"id"` 359 Type string `json:"type"` 360 Name string `json:"name"` 361 Content string `json:"content"` 362 Proxiable bool `json:"proxiable"` 363 Proxied bool `json:"proxied"` 364 TTL uint32 `json:"ttl"` 365 Locked bool `json:"locked"` 366 ZoneID string `json:"zone_id"` 367 ZoneName string `json:"zone_name"` 368 CreatedOn time.Time `json:"created_on"` 369 ModifiedOn time.Time `json:"modified_on"` 370 Data *cfRecData `json:"data"` 371 Priority json.Number `json:"priority"` 372 } 373 374 func (c *cfRecord) nativeToRecord(domain string) *models.RecordConfig { 375 // normalize cname,mx,ns records with dots to be consistent with our config format. 376 if c.Type == "CNAME" || c.Type == "MX" || c.Type == "NS" || c.Type == "SRV" { 377 c.Content = dnsutil.AddOrigin(c.Content+".", domain) 378 } 379 380 rc := &models.RecordConfig{ 381 TTL: c.TTL, 382 Original: c, 383 } 384 rc.SetLabelFromFQDN(c.Name, domain) 385 switch rType := c.Type; rType { // #rtype_variations 386 case "MX": 387 var priority uint16 388 if p, err := c.Priority.Int64(); err != nil { 389 panic(errors.Wrap(err, "error decoding priority from cloudflare record")) 390 } else { 391 priority = uint16(p) 392 } 393 if err := rc.SetTargetMX(priority, c.Content); err != nil { 394 panic(errors.Wrap(err, "unparsable MX record received from cloudflare")) 395 } 396 case "SRV": 397 data := *c.Data 398 if err := rc.SetTargetSRV(data.Priority, data.Weight, data.Port, 399 dnsutil.AddOrigin(data.Target+".", domain)); err != nil { 400 panic(errors.Wrap(err, "unparsable SRV record received from cloudflare")) 401 } 402 default: // "A", "AAAA", "ANAME", "CAA", "CNAME", "NS", "PTR", "TXT" 403 if err := rc.PopulateFromString(rType, c.Content, domain); err != nil { 404 panic(errors.Wrap(err, "unparsable record received from cloudflare")) 405 } 406 } 407 408 return rc 409 } 410 411 func getProxyMetadata(r *models.RecordConfig) map[string]string { 412 if r.Type != "A" && r.Type != "AAAA" && r.Type != "CNAME" { 413 return nil 414 } 415 proxied := false 416 if r.Original != nil { 417 proxied = r.Original.(*cfRecord).Proxied 418 } else { 419 proxied = r.Metadata[metaProxy] != "off" 420 } 421 return map[string]string{ 422 "proxy": fmt.Sprint(proxied), 423 } 424 } 425 426 // EnsureDomainExists returns an error of domain does not exist. 427 func (c *CloudflareApi) EnsureDomainExists(domain string) error { 428 if _, ok := c.domainIndex[domain]; ok { 429 return nil 430 } 431 var id string 432 id, err := c.createZone(domain) 433 fmt.Printf("Added zone for %s to Cloudflare account: %s\n", domain, id) 434 return err 435 }