github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/cloudflare/rest.go (about) 1 package cloudflare 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/StackExchange/dnscontrol/v2/models" 14 ) 15 16 const ( 17 baseURL = "https://api.cloudflare.com/client/v4/" 18 zonesURL = baseURL + "zones/" 19 recordsURL = zonesURL + "%s/dns_records/" 20 pageRulesURL = zonesURL + "%s/pagerules/" 21 singlePageRuleURL = pageRulesURL + "%s" 22 singleRecordURL = recordsURL + "%s" 23 ) 24 25 // get list of domains for account. Cache so the ids can be looked up from domain name 26 func (c *CloudflareApi) fetchDomainList() error { 27 c.domainIndex = map[string]string{} 28 c.nameservers = map[string][]string{} 29 page := 1 30 for { 31 zr := &zoneResponse{} 32 url := fmt.Sprintf("%s?page=%d&per_page=50", zonesURL, page) 33 if err := c.get(url, zr); err != nil { 34 return fmt.Errorf("Error fetching domain list from cloudflare: %s", err) 35 } 36 if !zr.Success { 37 return fmt.Errorf("Error fetching domain list from cloudflare: %s", stringifyErrors(zr.Errors)) 38 } 39 for _, zone := range zr.Result { 40 c.domainIndex[zone.Name] = zone.ID 41 for _, ns := range zone.Nameservers { 42 c.nameservers[zone.Name] = append(c.nameservers[zone.Name], ns) 43 } 44 } 45 ri := zr.ResultInfo 46 if len(zr.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount { 47 break 48 } 49 page++ 50 } 51 return nil 52 } 53 54 // get all records for a domain 55 func (c *CloudflareApi) getRecordsForDomain(id string, domain string) ([]*models.RecordConfig, error) { 56 url := fmt.Sprintf(recordsURL, id) 57 page := 1 58 records := []*models.RecordConfig{} 59 for { 60 reqURL := fmt.Sprintf("%s?page=%d&per_page=100", url, page) 61 var data recordsResponse 62 if err := c.get(reqURL, &data); err != nil { 63 return nil, fmt.Errorf("Error fetching record list from cloudflare: %s", err) 64 } 65 if !data.Success { 66 return nil, fmt.Errorf("Error fetching record list cloudflare: %s", stringifyErrors(data.Errors)) 67 } 68 for _, rec := range data.Result { 69 // fmt.Printf("REC: %+v\n", rec) 70 records = append(records, rec.nativeToRecord(domain)) 71 } 72 ri := data.ResultInfo 73 if len(data.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount { 74 break 75 } 76 page++ 77 } 78 // fmt.Printf("DEBUG REORDS=%v\n", records) 79 return records, nil 80 } 81 82 // create a correction to delete a record 83 func (c *CloudflareApi) deleteRec(rec *cfRecord, domainID string) *models.Correction { 84 return &models.Correction{ 85 Msg: fmt.Sprintf("DELETE record: %s %s %d %s (id=%s)", rec.Name, rec.Type, rec.TTL, rec.Content, rec.ID), 86 F: func() error { 87 endpoint := fmt.Sprintf(singleRecordURL, domainID, rec.ID) 88 req, err := http.NewRequest("DELETE", endpoint, nil) 89 if err != nil { 90 return err 91 } 92 c.setHeaders(req) 93 _, err = handleActionResponse(http.DefaultClient.Do(req)) 94 return err 95 }, 96 } 97 } 98 99 func (c *CloudflareApi) createZone(domainName string) (string, error) { 100 type createZone struct { 101 Name string `json:"name"` 102 103 Account struct { 104 ID string `json:"id"` 105 Name string `json:"name"` 106 } `json:"account"` 107 } 108 var id string 109 cz := &createZone{ 110 Name: domainName} 111 112 if c.AccountID != "" || c.AccountName != "" { 113 cz.Account.ID = c.AccountID 114 cz.Account.Name = c.AccountName 115 } 116 117 buf := &bytes.Buffer{} 118 encoder := json.NewEncoder(buf) 119 if err := encoder.Encode(cz); err != nil { 120 return "", err 121 } 122 req, err := http.NewRequest("POST", zonesURL, buf) 123 if err != nil { 124 return "", err 125 } 126 c.setHeaders(req) 127 id, err = handleActionResponse(http.DefaultClient.Do(req)) 128 return id, err 129 } 130 131 func cfSrvData(rec *models.RecordConfig) *cfRecData { 132 serverParts := strings.Split(rec.GetLabelFQDN(), ".") 133 c := &cfRecData{ 134 Service: serverParts[0], 135 Proto: serverParts[1], 136 Name: strings.Join(serverParts[2:], "."), 137 Port: rec.SrvPort, 138 Priority: rec.SrvPriority, 139 Weight: rec.SrvWeight, 140 } 141 c.Target = cfTarget(rec.GetTargetField()) 142 return c 143 } 144 145 func cfCaaData(rec *models.RecordConfig) *cfRecData { 146 return &cfRecData{ 147 Tag: rec.CaaTag, 148 Flags: rec.CaaFlag, 149 Value: rec.GetTargetField(), 150 } 151 } 152 153 func cfTlsaData(rec *models.RecordConfig) *cfRecData { 154 return &cfRecData{ 155 Usage: rec.TlsaUsage, 156 Selector: rec.TlsaSelector, 157 Matching_Type: rec.TlsaMatchingType, 158 Certificate: rec.GetTargetField(), 159 } 160 } 161 162 func cfSshfpData(rec *models.RecordConfig) *cfRecData { 163 return &cfRecData{ 164 Algorithm: rec.SshfpAlgorithm, 165 Hash_Type: rec.SshfpFingerprint, 166 Fingerprint: rec.GetTargetField(), 167 } 168 } 169 170 func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*models.Correction { 171 type createRecord struct { 172 Name string `json:"name"` 173 Type string `json:"type"` 174 Content string `json:"content"` 175 TTL uint32 `json:"ttl"` 176 Priority uint16 `json:"priority"` 177 Data *cfRecData `json:"data"` 178 } 179 var id string 180 content := rec.GetTargetField() 181 if rec.Metadata[metaOriginalIP] != "" { 182 content = rec.Metadata[metaOriginalIP] 183 } 184 prio := "" 185 if rec.Type == "MX" { 186 prio = fmt.Sprintf(" %d ", rec.MxPreference) 187 } 188 arr := []*models.Correction{{ 189 Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.GetLabel(), rec.Type, rec.TTL, prio, content), 190 F: func() error { 191 192 cf := &createRecord{ 193 Name: rec.GetLabel(), 194 Type: rec.Type, 195 TTL: rec.TTL, 196 Content: content, 197 Priority: rec.MxPreference, 198 } 199 if rec.Type == "SRV" { 200 cf.Data = cfSrvData(rec) 201 cf.Name = rec.GetLabelFQDN() 202 } else if rec.Type == "CAA" { 203 cf.Data = cfCaaData(rec) 204 cf.Name = rec.GetLabelFQDN() 205 cf.Content = "" 206 } else if rec.Type == "TLSA" { 207 cf.Data = cfTlsaData(rec) 208 cf.Name = rec.GetLabelFQDN() 209 } else if rec.Type == "SSHFP" { 210 cf.Data = cfSshfpData(rec) 211 cf.Name = rec.GetLabelFQDN() 212 } 213 endpoint := fmt.Sprintf(recordsURL, domainID) 214 buf := &bytes.Buffer{} 215 encoder := json.NewEncoder(buf) 216 if err := encoder.Encode(cf); err != nil { 217 return err 218 } 219 req, err := http.NewRequest("POST", endpoint, buf) 220 if err != nil { 221 return err 222 } 223 c.setHeaders(req) 224 id, err = handleActionResponse(http.DefaultClient.Do(req)) 225 return err 226 }, 227 }} 228 if rec.Metadata[metaProxy] != "off" { 229 arr = append(arr, &models.Correction{ 230 Msg: fmt.Sprintf("ACTIVATE PROXY for new record %s %s %d %s", rec.GetLabel(), rec.Type, rec.TTL, rec.GetTargetField()), 231 F: func() error { return c.modifyRecord(domainID, id, true, rec) }, 232 }) 233 } 234 return arr 235 } 236 237 func (c *CloudflareApi) modifyRecord(domainID, recID string, proxied bool, rec *models.RecordConfig) error { 238 if domainID == "" || recID == "" { 239 return fmt.Errorf("cannot modify record if domain or record id are empty") 240 } 241 type record struct { 242 ID string `json:"id"` 243 Proxied bool `json:"proxied"` 244 Name string `json:"name"` 245 Type string `json:"type"` 246 Content string `json:"content"` 247 Priority uint16 `json:"priority"` 248 TTL uint32 `json:"ttl"` 249 Data *cfRecData `json:"data"` 250 } 251 r := record{ 252 ID: recID, 253 Proxied: proxied, 254 Name: rec.GetLabel(), 255 Type: rec.Type, 256 Content: rec.GetTargetField(), 257 Priority: rec.MxPreference, 258 TTL: rec.TTL, 259 Data: nil, 260 } 261 if rec.Type == "SRV" { 262 r.Data = cfSrvData(rec) 263 r.Name = rec.GetLabelFQDN() 264 } else if rec.Type == "CAA" { 265 r.Data = cfCaaData(rec) 266 r.Name = rec.GetLabelFQDN() 267 r.Content = "" 268 } else if rec.Type == "TLSA" { 269 r.Data = cfTlsaData(rec) 270 r.Name = rec.GetLabelFQDN() 271 } else if rec.Type == "SSHFP" { 272 r.Data = cfSshfpData(rec) 273 r.Name = rec.GetLabelFQDN() 274 } 275 endpoint := fmt.Sprintf(singleRecordURL, domainID, recID) 276 buf := &bytes.Buffer{} 277 encoder := json.NewEncoder(buf) 278 if err := encoder.Encode(r); err != nil { 279 return err 280 } 281 req, err := http.NewRequest("PUT", endpoint, buf) 282 if err != nil { 283 return err 284 } 285 c.setHeaders(req) 286 _, err = handleActionResponse(http.DefaultClient.Do(req)) 287 return err 288 } 289 290 // change universal ssl state 291 func (c *CloudflareApi) changeUniversalSSL(domainID string, state bool) error { 292 type setUniversalSSL struct { 293 Enabled bool `json:"enabled"` 294 } 295 us := &setUniversalSSL{ 296 Enabled: state, 297 } 298 299 // create json 300 buf := &bytes.Buffer{} 301 encoder := json.NewEncoder(buf) 302 if err := encoder.Encode(us); err != nil { 303 return err 304 } 305 306 // send request. 307 endpoint := fmt.Sprintf(zonesURL+"%s/ssl/universal/settings", domainID) 308 req, err := http.NewRequest("PATCH", endpoint, buf) 309 if err != nil { 310 return err 311 } 312 c.setHeaders(req) 313 _, err = handleActionResponse(http.DefaultClient.Do(req)) 314 315 return err 316 } 317 318 // change universal ssl state 319 func (c *CloudflareApi) getUniversalSSL(domainID string) (bool, error) { 320 type universalSSLResponse struct { 321 Success bool `json:"success"` 322 Errors []interface{} `json:"errors"` 323 Messages []interface{} `json:"messages"` 324 Result struct { 325 Enabled bool `json:"enabled"` 326 } `json:"result"` 327 } 328 329 // send request. 330 endpoint := fmt.Sprintf(zonesURL+"%s/ssl/universal/settings", domainID) 331 var result universalSSLResponse 332 err := c.get(endpoint, &result) 333 if err != nil { 334 return true, err 335 } 336 337 return result.Result.Enabled, err 338 } 339 340 // common error handling for all action responses 341 func handleActionResponse(resp *http.Response, err error) (id string, e error) { 342 if err != nil { 343 return "", err 344 } 345 defer resp.Body.Close() 346 result := &basicResponse{} 347 decoder := json.NewDecoder(resp.Body) 348 if err = decoder.Decode(result); err != nil { 349 return "", fmt.Errorf("Unknown error. Status code: %d", resp.StatusCode) 350 } 351 if resp.StatusCode != 200 { 352 return "", fmt.Errorf(stringifyErrors(result.Errors)) 353 } 354 return result.Result.ID, nil 355 } 356 357 func (c *CloudflareApi) setHeaders(req *http.Request) { 358 if len(c.ApiToken) > 0 { 359 req.Header.Set("Authorization", "Bearer "+c.ApiToken) 360 } else { 361 req.Header.Set("X-Auth-Key", c.ApiKey) 362 req.Header.Set("X-Auth-Email", c.ApiUser) 363 } 364 } 365 366 // generic get handler. makes request and unmarshalls response to given interface 367 func (c *CloudflareApi) get(endpoint string, target interface{}) error { 368 req, err := http.NewRequest("GET", endpoint, nil) 369 if err != nil { 370 return err 371 } 372 c.setHeaders(req) 373 resp, err := http.DefaultClient.Do(req) 374 if err != nil { 375 return err 376 } 377 defer resp.Body.Close() 378 if resp.StatusCode != 200 { 379 dat, _ := ioutil.ReadAll(resp.Body) 380 fmt.Println(string(dat)) 381 return fmt.Errorf("bad status code from cloudflare: %d not 200", resp.StatusCode) 382 } 383 decoder := json.NewDecoder(resp.Body) 384 return decoder.Decode(target) 385 } 386 387 func (c *CloudflareApi) getPageRules(id string, domain string) ([]*models.RecordConfig, error) { 388 url := fmt.Sprintf(pageRulesURL, id) 389 data := pageRuleResponse{} 390 if err := c.get(url, &data); err != nil { 391 return nil, fmt.Errorf("Error fetching page rule list from cloudflare: %s", err) 392 } 393 if !data.Success { 394 return nil, fmt.Errorf("Error fetching page rule list cloudflare: %s", stringifyErrors(data.Errors)) 395 } 396 recs := []*models.RecordConfig{} 397 for _, pr := range data.Result { 398 // only interested in forwarding rules. Lets be very specific, and skip anything else 399 if len(pr.Actions) != 1 || len(pr.Targets) != 1 { 400 continue 401 } 402 if pr.Actions[0].ID != "forwarding_url" { 403 continue 404 } 405 err := json.Unmarshal([]byte(pr.Actions[0].Value), &pr.ForwardingInfo) 406 if err != nil { 407 return nil, err 408 } 409 var thisPr = pr 410 r := &models.RecordConfig{ 411 Type: "PAGE_RULE", 412 Original: thisPr, 413 TTL: 1, 414 } 415 r.SetLabel("@", domain) 416 r.SetTarget(fmt.Sprintf("%s,%s,%d,%d", // $FROM,$TO,$PRIO,$CODE 417 pr.Targets[0].Constraint.Value, 418 pr.ForwardingInfo.URL, 419 pr.Priority, 420 pr.ForwardingInfo.StatusCode)) 421 recs = append(recs, r) 422 } 423 return recs, nil 424 } 425 426 func (c *CloudflareApi) deletePageRule(recordID, domainID string) error { 427 endpoint := fmt.Sprintf(singlePageRuleURL, domainID, recordID) 428 req, err := http.NewRequest("DELETE", endpoint, nil) 429 if err != nil { 430 return err 431 } 432 c.setHeaders(req) 433 _, err = handleActionResponse(http.DefaultClient.Do(req)) 434 return err 435 } 436 437 func (c *CloudflareApi) updatePageRule(recordID, domainID string, target string) error { 438 if err := c.deletePageRule(recordID, domainID); err != nil { 439 return err 440 } 441 return c.createPageRule(domainID, target) 442 } 443 444 func (c *CloudflareApi) createPageRule(domainID string, target string) error { 445 endpoint := fmt.Sprintf(pageRulesURL, domainID) 446 return c.sendPageRule(endpoint, "POST", target) 447 } 448 449 func (c *CloudflareApi) sendPageRule(endpoint, method string, data string) error { 450 // from to priority code 451 parts := strings.Split(data, ",") 452 priority, _ := strconv.Atoi(parts[2]) 453 code, _ := strconv.Atoi(parts[3]) 454 fwdInfo := &pageRuleFwdInfo{ 455 StatusCode: code, 456 URL: parts[1], 457 } 458 dat, _ := json.Marshal(fwdInfo) 459 pr := &pageRule{ 460 Status: "active", 461 Priority: priority, 462 Targets: []pageRuleTarget{ 463 {Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: parts[0]}}, 464 }, 465 Actions: []pageRuleAction{ 466 {ID: "forwarding_url", Value: json.RawMessage(dat)}, 467 }, 468 } 469 buf := &bytes.Buffer{} 470 enc := json.NewEncoder(buf) 471 if err := enc.Encode(pr); err != nil { 472 return err 473 } 474 req, err := http.NewRequest(method, endpoint, buf) 475 if err != nil { 476 return err 477 } 478 c.setHeaders(req) 479 _, err = handleActionResponse(http.DefaultClient.Do(req)) 480 return err 481 } 482 483 func stringifyErrors(errors []interface{}) string { 484 dat, err := json.Marshal(errors) 485 if err != nil { 486 return "???" 487 } 488 return string(dat) 489 } 490 491 type recordsResponse struct { 492 basicResponse 493 Result []*cfRecord `json:"result"` 494 ResultInfo pagingInfo `json:"result_info"` 495 } 496 497 type basicResponse struct { 498 Success bool `json:"success"` 499 Errors []interface{} `json:"errors"` 500 Messages []interface{} `json:"messages"` 501 Result struct { 502 ID string `json:"id"` 503 } `json:"result"` 504 } 505 506 type pageRuleResponse struct { 507 basicResponse 508 Result []*pageRule `json:"result"` 509 ResultInfo pagingInfo `json:"result_info"` 510 } 511 512 type pageRule struct { 513 ID string `json:"id,omitempty"` 514 Targets []pageRuleTarget `json:"targets"` 515 Actions []pageRuleAction `json:"actions"` 516 Priority int `json:"priority"` 517 Status string `json:"status"` 518 ModifiedOn time.Time `json:"modified_on,omitempty"` 519 CreatedOn time.Time `json:"created_on,omitempty"` 520 ForwardingInfo *pageRuleFwdInfo `json:"-"` 521 } 522 523 type pageRuleTarget struct { 524 Target string `json:"target"` 525 Constraint pageRuleConstraint `json:"constraint"` 526 } 527 528 type pageRuleConstraint struct { 529 Operator string `json:"operator"` 530 Value string `json:"value"` 531 } 532 533 type pageRuleAction struct { 534 ID string `json:"id"` 535 Value json.RawMessage `json:"value"` 536 } 537 538 type pageRuleFwdInfo struct { 539 URL string `json:"url"` 540 StatusCode int `json:"status_code"` 541 } 542 543 type zoneResponse struct { 544 basicResponse 545 Result []struct { 546 ID string `json:"id"` 547 Name string `json:"name"` 548 Nameservers []string `json:"name_servers"` 549 } `json:"result"` 550 ResultInfo pagingInfo `json:"result_info"` 551 } 552 553 type pagingInfo struct { 554 Page int `json:"page"` 555 PerPage int `json:"per_page"` 556 Count int `json:"count"` 557 TotalCount int `json:"total_count"` 558 }