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