github.com/StackExchange/DNSControl@v0.2.8/providers/cloudflare/rest.go (about) 1 package cloudflare 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/StackExchange/dnscontrol/models" 13 "github.com/pkg/errors" 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 errors.Errorf("Error fetching domain list from cloudflare: %s", err) 35 } 36 if !zr.Success { 37 return errors.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, errors.Errorf("Error fetching record list from cloudflare: %s", err) 64 } 65 if !data.Success { 66 return nil, errors.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 return &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 Target: rec.GetTargetField(), 141 } 142 } 143 144 func cfCaaData(rec *models.RecordConfig) *cfRecData { 145 return &cfRecData{ 146 Tag: rec.CaaTag, 147 Flags: rec.CaaFlag, 148 Value: rec.GetTargetField(), 149 } 150 } 151 152 func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*models.Correction { 153 type createRecord struct { 154 Name string `json:"name"` 155 Type string `json:"type"` 156 Content string `json:"content"` 157 TTL uint32 `json:"ttl"` 158 Priority uint16 `json:"priority"` 159 Data *cfRecData `json:"data"` 160 } 161 var id string 162 content := rec.GetTargetField() 163 if rec.Metadata[metaOriginalIP] != "" { 164 content = rec.Metadata[metaOriginalIP] 165 } 166 prio := "" 167 if rec.Type == "MX" { 168 prio = fmt.Sprintf(" %d ", rec.MxPreference) 169 } 170 arr := []*models.Correction{{ 171 Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.GetLabel(), rec.Type, rec.TTL, prio, content), 172 F: func() error { 173 174 cf := &createRecord{ 175 Name: rec.GetLabel(), 176 Type: rec.Type, 177 TTL: rec.TTL, 178 Content: content, 179 Priority: rec.MxPreference, 180 } 181 if rec.Type == "SRV" { 182 cf.Data = cfSrvData(rec) 183 cf.Name = rec.GetLabelFQDN() 184 } else if rec.Type == "CAA" { 185 cf.Data = cfCaaData(rec) 186 cf.Name = rec.GetLabelFQDN() 187 cf.Content = "" 188 } 189 endpoint := fmt.Sprintf(recordsURL, domainID) 190 buf := &bytes.Buffer{} 191 encoder := json.NewEncoder(buf) 192 if err := encoder.Encode(cf); err != nil { 193 return err 194 } 195 req, err := http.NewRequest("POST", endpoint, buf) 196 if err != nil { 197 return err 198 } 199 c.setHeaders(req) 200 id, err = handleActionResponse(http.DefaultClient.Do(req)) 201 return err 202 }, 203 }} 204 if rec.Metadata[metaProxy] != "off" { 205 arr = append(arr, &models.Correction{ 206 Msg: fmt.Sprintf("ACTIVATE PROXY for new record %s %s %d %s", rec.GetLabel(), rec.Type, rec.TTL, rec.GetTargetField()), 207 F: func() error { return c.modifyRecord(domainID, id, true, rec) }, 208 }) 209 } 210 return arr 211 } 212 213 func (c *CloudflareApi) modifyRecord(domainID, recID string, proxied bool, rec *models.RecordConfig) error { 214 if domainID == "" || recID == "" { 215 return errors.Errorf("cannot modify record if domain or record id are empty") 216 } 217 type record struct { 218 ID string `json:"id"` 219 Proxied bool `json:"proxied"` 220 Name string `json:"name"` 221 Type string `json:"type"` 222 Content string `json:"content"` 223 Priority uint16 `json:"priority"` 224 TTL uint32 `json:"ttl"` 225 Data *cfRecData `json:"data"` 226 } 227 r := record{ 228 ID: recID, 229 Proxied: proxied, 230 Name: rec.GetLabel(), 231 Type: rec.Type, 232 Content: rec.GetTargetField(), 233 Priority: rec.MxPreference, 234 TTL: rec.TTL, 235 Data: nil, 236 } 237 if rec.Type == "SRV" { 238 r.Data = cfSrvData(rec) 239 r.Name = rec.GetLabelFQDN() 240 } else if rec.Type == "CAA" { 241 r.Data = cfCaaData(rec) 242 r.Name = rec.GetLabelFQDN() 243 r.Content = "" 244 } 245 endpoint := fmt.Sprintf(singleRecordURL, domainID, recID) 246 buf := &bytes.Buffer{} 247 encoder := json.NewEncoder(buf) 248 if err := encoder.Encode(r); err != nil { 249 return err 250 } 251 req, err := http.NewRequest("PUT", endpoint, buf) 252 if err != nil { 253 return err 254 } 255 c.setHeaders(req) 256 _, err = handleActionResponse(http.DefaultClient.Do(req)) 257 return err 258 } 259 260 // common error handling for all action responses 261 func handleActionResponse(resp *http.Response, err error) (id string, e error) { 262 if err != nil { 263 return "", err 264 } 265 defer resp.Body.Close() 266 result := &basicResponse{} 267 decoder := json.NewDecoder(resp.Body) 268 if err = decoder.Decode(result); err != nil { 269 return "", errors.Errorf("Unknown error. Status code: %d", resp.StatusCode) 270 } 271 if resp.StatusCode != 200 { 272 return "", errors.Errorf(stringifyErrors(result.Errors)) 273 } 274 return result.Result.ID, nil 275 } 276 277 func (c *CloudflareApi) setHeaders(req *http.Request) { 278 req.Header.Set("X-Auth-Key", c.ApiKey) 279 req.Header.Set("X-Auth-Email", c.ApiUser) 280 } 281 282 // generic get handler. makes request and unmarshalls response to given interface 283 func (c *CloudflareApi) get(endpoint string, target interface{}) error { 284 req, err := http.NewRequest("GET", endpoint, nil) 285 if err != nil { 286 return err 287 } 288 c.setHeaders(req) 289 resp, err := http.DefaultClient.Do(req) 290 if err != nil { 291 return err 292 } 293 defer resp.Body.Close() 294 if resp.StatusCode != 200 { 295 return errors.Errorf("bad status code from cloudflare: %d not 200", resp.StatusCode) 296 } 297 decoder := json.NewDecoder(resp.Body) 298 return decoder.Decode(target) 299 } 300 301 func (c *CloudflareApi) getPageRules(id string, domain string) ([]*models.RecordConfig, error) { 302 url := fmt.Sprintf(pageRulesURL, id) 303 data := pageRuleResponse{} 304 if err := c.get(url, &data); err != nil { 305 return nil, errors.Errorf("Error fetching page rule list from cloudflare: %s", err) 306 } 307 if !data.Success { 308 return nil, errors.Errorf("Error fetching page rule list cloudflare: %s", stringifyErrors(data.Errors)) 309 } 310 recs := []*models.RecordConfig{} 311 for _, pr := range data.Result { 312 // only interested in forwarding rules. Lets be very specific, and skip anything else 313 if len(pr.Actions) != 1 || len(pr.Targets) != 1 { 314 continue 315 } 316 if pr.Actions[0].ID != "forwarding_url" { 317 continue 318 } 319 err := json.Unmarshal([]byte(pr.Actions[0].Value), &pr.ForwardingInfo) 320 if err != nil { 321 return nil, err 322 } 323 var thisPr = pr 324 r := &models.RecordConfig{ 325 Type: "PAGE_RULE", 326 Original: thisPr, 327 TTL: 1, 328 } 329 r.SetLabel("@", domain) 330 r.SetTarget(fmt.Sprintf("%s,%s,%d,%d", // $FROM,$TO,$PRIO,$CODE 331 pr.Targets[0].Constraint.Value, 332 pr.ForwardingInfo.URL, 333 pr.Priority, 334 pr.ForwardingInfo.StatusCode)) 335 recs = append(recs, r) 336 } 337 return recs, nil 338 } 339 340 func (c *CloudflareApi) deletePageRule(recordID, domainID string) error { 341 endpoint := fmt.Sprintf(singlePageRuleURL, domainID, recordID) 342 req, err := http.NewRequest("DELETE", endpoint, nil) 343 if err != nil { 344 return err 345 } 346 c.setHeaders(req) 347 _, err = handleActionResponse(http.DefaultClient.Do(req)) 348 return err 349 } 350 351 func (c *CloudflareApi) updatePageRule(recordID, domainID string, target string) error { 352 if err := c.deletePageRule(recordID, domainID); err != nil { 353 return err 354 } 355 return c.createPageRule(domainID, target) 356 } 357 358 func (c *CloudflareApi) createPageRule(domainID string, target string) error { 359 endpoint := fmt.Sprintf(pageRulesURL, domainID) 360 return c.sendPageRule(endpoint, "POST", target) 361 } 362 363 func (c *CloudflareApi) sendPageRule(endpoint, method string, data string) error { 364 // from to priority code 365 parts := strings.Split(data, ",") 366 priority, _ := strconv.Atoi(parts[2]) 367 code, _ := strconv.Atoi(parts[3]) 368 fwdInfo := &pageRuleFwdInfo{ 369 StatusCode: code, 370 URL: parts[1], 371 } 372 dat, _ := json.Marshal(fwdInfo) 373 pr := &pageRule{ 374 Status: "active", 375 Priority: priority, 376 Targets: []pageRuleTarget{ 377 {Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: parts[0]}}, 378 }, 379 Actions: []pageRuleAction{ 380 {ID: "forwarding_url", Value: json.RawMessage(dat)}, 381 }, 382 } 383 buf := &bytes.Buffer{} 384 enc := json.NewEncoder(buf) 385 if err := enc.Encode(pr); err != nil { 386 return err 387 } 388 req, err := http.NewRequest(method, endpoint, buf) 389 if err != nil { 390 return err 391 } 392 c.setHeaders(req) 393 _, err = handleActionResponse(http.DefaultClient.Do(req)) 394 return err 395 } 396 397 func stringifyErrors(errors []interface{}) string { 398 dat, err := json.Marshal(errors) 399 if err != nil { 400 return "???" 401 } 402 return string(dat) 403 } 404 405 type recordsResponse struct { 406 basicResponse 407 Result []*cfRecord `json:"result"` 408 ResultInfo pagingInfo `json:"result_info"` 409 } 410 411 type basicResponse struct { 412 Success bool `json:"success"` 413 Errors []interface{} `json:"errors"` 414 Messages []interface{} `json:"messages"` 415 Result struct { 416 ID string `json:"id"` 417 } `json:"result"` 418 } 419 420 type pageRuleResponse struct { 421 basicResponse 422 Result []*pageRule `json:"result"` 423 ResultInfo pagingInfo `json:"result_info"` 424 } 425 426 type pageRule struct { 427 ID string `json:"id,omitempty"` 428 Targets []pageRuleTarget `json:"targets"` 429 Actions []pageRuleAction `json:"actions"` 430 Priority int `json:"priority"` 431 Status string `json:"status"` 432 ModifiedOn time.Time `json:"modified_on,omitempty"` 433 CreatedOn time.Time `json:"created_on,omitempty"` 434 ForwardingInfo *pageRuleFwdInfo `json:"-"` 435 } 436 437 type pageRuleTarget struct { 438 Target string `json:"target"` 439 Constraint pageRuleConstraint `json:"constraint"` 440 } 441 442 type pageRuleConstraint struct { 443 Operator string `json:"operator"` 444 Value string `json:"value"` 445 } 446 447 type pageRuleAction struct { 448 ID string `json:"id"` 449 Value json.RawMessage `json:"value"` 450 } 451 452 type pageRuleFwdInfo struct { 453 URL string `json:"url"` 454 StatusCode int `json:"status_code"` 455 } 456 457 type zoneResponse struct { 458 basicResponse 459 Result []struct { 460 ID string `json:"id"` 461 Name string `json:"name"` 462 Nameservers []string `json:"name_servers"` 463 } `json:"result"` 464 ResultInfo pagingInfo `json:"result_info"` 465 } 466 467 type pagingInfo struct { 468 Page int `json:"page"` 469 PerPage int `json:"per_page"` 470 Count int `json:"count"` 471 TotalCount int `json:"total_count"` 472 }