github.com/pmoroney/dnscontrol@v0.2.4-0.20171024134423-fad98f73f44a/providers/cloudflare/rest.go (about) 1 package cloudflare 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "time" 9 10 "strings" 11 12 "strconv" 13 14 "github.com/StackExchange/dnscontrol/models" 15 ) 16 17 const ( 18 baseURL = "https://api.cloudflare.com/client/v4/" 19 zonesURL = baseURL + "zones/" 20 recordsURL = zonesURL + "%s/dns_records/" 21 pageRulesURL = zonesURL + "%s/pagerules/" 22 singlePageRuleURL = pageRulesURL + "%s" 23 singleRecordURL = recordsURL + "%s" 24 ) 25 26 // get list of domains for account. Cache so the ids can be looked up from domain name 27 func (c *CloudflareApi) fetchDomainList() error { 28 c.domainIndex = map[string]string{} 29 c.nameservers = map[string][]string{} 30 page := 1 31 for { 32 zr := &zoneResponse{} 33 url := fmt.Sprintf("%s?page=%d&per_page=50", zonesURL, page) 34 if err := c.get(url, zr); err != nil { 35 return fmt.Errorf("Error fetching domain list from cloudflare: %s", err) 36 } 37 if !zr.Success { 38 return fmt.Errorf("Error fetching domain list from cloudflare: %s", stringifyErrors(zr.Errors)) 39 } 40 for _, zone := range zr.Result { 41 c.domainIndex[zone.Name] = zone.ID 42 for _, ns := range zone.Nameservers { 43 c.nameservers[zone.Name] = append(c.nameservers[zone.Name], ns) 44 } 45 } 46 ri := zr.ResultInfo 47 if len(zr.Result) == 0 || ri.Page*ri.PerPage >= ri.TotalCount { 48 break 49 } 50 page++ 51 } 52 return nil 53 } 54 55 // get all records for a domain 56 func (c *CloudflareApi) getRecordsForDomain(id string, domain string) ([]*models.RecordConfig, error) { 57 url := fmt.Sprintf(recordsURL, id) 58 page := 1 59 records := []*models.RecordConfig{} 60 for { 61 reqURL := fmt.Sprintf("%s?page=%d&per_page=100", url, page) 62 var data recordsResponse 63 if err := c.get(reqURL, &data); err != nil { 64 return nil, fmt.Errorf("Error fetching record list from cloudflare: %s", err) 65 } 66 if !data.Success { 67 return nil, fmt.Errorf("Error fetching record list cloudflare: %s", stringifyErrors(data.Errors)) 68 } 69 for _, rec := range data.Result { 70 records = append(records, rec.toRecord(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 return records, nil 79 } 80 81 // create a correction to delete a record 82 func (c *CloudflareApi) deleteRec(rec *cfRecord, domainID string) *models.Correction { 83 return &models.Correction{ 84 Msg: fmt.Sprintf("DELETE record: %s %s %d %s (id=%s)", rec.Name, rec.Type, rec.TTL, rec.Content, rec.ID), 85 F: func() error { 86 endpoint := fmt.Sprintf(singleRecordURL, domainID, rec.ID) 87 req, err := http.NewRequest("DELETE", endpoint, nil) 88 if err != nil { 89 return err 90 } 91 c.setHeaders(req) 92 _, err = handleActionResponse(http.DefaultClient.Do(req)) 93 return err 94 }, 95 } 96 } 97 98 func (c *CloudflareApi) createZone(domainName string) (string, error) { 99 type createZone struct { 100 Name string `json:"name"` 101 } 102 var id string 103 cz := &createZone{ 104 Name: domainName} 105 buf := &bytes.Buffer{} 106 encoder := json.NewEncoder(buf) 107 if err := encoder.Encode(cz); err != nil { 108 return "", err 109 } 110 req, err := http.NewRequest("POST", zonesURL, buf) 111 if err != nil { 112 return "", err 113 } 114 c.setHeaders(req) 115 id, err = handleActionResponse(http.DefaultClient.Do(req)) 116 return id, err 117 } 118 119 func cfSrvData(rec *models.RecordConfig) *cfRecData { 120 serverParts := strings.Split(rec.NameFQDN, ".") 121 return &cfRecData{ 122 Service: serverParts[0], 123 Proto: serverParts[1], 124 Name: strings.Join(serverParts[2:], "."), 125 Port: rec.SrvPort, 126 Priority: rec.SrvPriority, 127 Weight: rec.SrvWeight, 128 Target: rec.Target, 129 } 130 } 131 132 func (c *CloudflareApi) createRec(rec *models.RecordConfig, domainID string) []*models.Correction { 133 type createRecord struct { 134 Name string `json:"name"` 135 Type string `json:"type"` 136 Content string `json:"content"` 137 TTL uint32 `json:"ttl"` 138 Priority uint16 `json:"priority"` 139 Data *cfRecData `json:"data"` 140 } 141 var id string 142 content := rec.Target 143 if rec.Metadata[metaOriginalIP] != "" { 144 content = rec.Metadata[metaOriginalIP] 145 } 146 prio := "" 147 if rec.Type == "MX" { 148 prio = fmt.Sprintf(" %d ", rec.MxPreference) 149 } 150 arr := []*models.Correction{{ 151 Msg: fmt.Sprintf("CREATE record: %s %s %d%s %s", rec.Name, rec.Type, rec.TTL, prio, content), 152 F: func() error { 153 154 cf := &createRecord{ 155 Name: rec.Name, 156 Type: rec.Type, 157 TTL: rec.TTL, 158 Content: content, 159 Priority: rec.MxPreference, 160 } 161 if rec.Type == "SRV" { 162 cf.Data = cfSrvData(rec) 163 cf.Name = rec.NameFQDN 164 } 165 endpoint := fmt.Sprintf(recordsURL, domainID) 166 buf := &bytes.Buffer{} 167 encoder := json.NewEncoder(buf) 168 if err := encoder.Encode(cf); err != nil { 169 return err 170 } 171 req, err := http.NewRequest("POST", endpoint, buf) 172 if err != nil { 173 return err 174 } 175 c.setHeaders(req) 176 id, err = handleActionResponse(http.DefaultClient.Do(req)) 177 return err 178 }, 179 }} 180 if rec.Metadata[metaProxy] != "off" { 181 arr = append(arr, &models.Correction{ 182 Msg: fmt.Sprintf("ACTIVATE PROXY for new record %s %s %d %s", rec.Name, rec.Type, rec.TTL, rec.Target), 183 F: func() error { return c.modifyRecord(domainID, id, true, rec) }, 184 }) 185 } 186 return arr 187 } 188 189 func (c *CloudflareApi) modifyRecord(domainID, recID string, proxied bool, rec *models.RecordConfig) error { 190 if domainID == "" || recID == "" { 191 return fmt.Errorf("Cannot modify record if domain or record id are empty.") 192 } 193 type record struct { 194 ID string `json:"id"` 195 Proxied bool `json:"proxied"` 196 Name string `json:"name"` 197 Type string `json:"type"` 198 Content string `json:"content"` 199 Priority uint16 `json:"priority"` 200 TTL uint32 `json:"ttl"` 201 Data *cfRecData `json:"data"` 202 } 203 r := record{recID, proxied, rec.Name, rec.Type, rec.Target, rec.MxPreference, rec.TTL, nil} 204 if rec.Type == "SRV" { 205 r.Data = cfSrvData(rec) 206 r.Name = rec.NameFQDN 207 } 208 endpoint := fmt.Sprintf(singleRecordURL, domainID, recID) 209 buf := &bytes.Buffer{} 210 encoder := json.NewEncoder(buf) 211 if err := encoder.Encode(r); err != nil { 212 return err 213 } 214 req, err := http.NewRequest("PUT", endpoint, buf) 215 if err != nil { 216 return err 217 } 218 c.setHeaders(req) 219 _, err = handleActionResponse(http.DefaultClient.Do(req)) 220 return err 221 } 222 223 // common error handling for all action responses 224 func handleActionResponse(resp *http.Response, err error) (id string, e error) { 225 if err != nil { 226 return "", err 227 } 228 defer resp.Body.Close() 229 result := &basicResponse{} 230 decoder := json.NewDecoder(resp.Body) 231 if err = decoder.Decode(result); err != nil { 232 return "", fmt.Errorf("Unknown error. Status code: %d", resp.StatusCode) 233 } 234 if resp.StatusCode != 200 { 235 return "", fmt.Errorf(stringifyErrors(result.Errors)) 236 } 237 return result.Result.ID, nil 238 } 239 240 func (c *CloudflareApi) setHeaders(req *http.Request) { 241 req.Header.Set("X-Auth-Key", c.ApiKey) 242 req.Header.Set("X-Auth-Email", c.ApiUser) 243 } 244 245 // generic get handler. makes request and unmarshalls response to given interface 246 func (c *CloudflareApi) get(endpoint string, target interface{}) error { 247 req, err := http.NewRequest("GET", endpoint, nil) 248 if err != nil { 249 return err 250 } 251 c.setHeaders(req) 252 resp, err := http.DefaultClient.Do(req) 253 if err != nil { 254 return err 255 } 256 defer resp.Body.Close() 257 if resp.StatusCode != 200 { 258 return fmt.Errorf("Bad status code from cloudflare: %d not 200.", resp.StatusCode) 259 } 260 decoder := json.NewDecoder(resp.Body) 261 return decoder.Decode(target) 262 } 263 264 func (c *CloudflareApi) getPageRules(id string, domain string) ([]*models.RecordConfig, error) { 265 url := fmt.Sprintf(pageRulesURL, id) 266 data := pageRuleResponse{} 267 if err := c.get(url, &data); err != nil { 268 return nil, fmt.Errorf("Error fetching page rule list from cloudflare: %s", err) 269 } 270 if !data.Success { 271 return nil, fmt.Errorf("Error fetching page rule list cloudflare: %s", stringifyErrors(data.Errors)) 272 } 273 recs := []*models.RecordConfig{} 274 for _, pr := range data.Result { 275 // only interested in forwarding rules. Lets be very specific, and skip anything else 276 if len(pr.Actions) != 1 || len(pr.Targets) != 1 { 277 continue 278 } 279 if pr.Actions[0].ID != "forwarding_url" { 280 continue 281 } 282 err := json.Unmarshal([]byte(pr.Actions[0].Value), &pr.ForwardingInfo) 283 if err != nil { 284 return nil, err 285 } 286 var thisPr = pr 287 recs = append(recs, &models.RecordConfig{ 288 Name: "@", 289 NameFQDN: domain, 290 Type: "PAGE_RULE", 291 //$FROM,$TO,$PRIO,$CODE 292 Target: fmt.Sprintf("%s,%s,%d,%d", pr.Targets[0].Constraint.Value, pr.ForwardingInfo.URL, pr.Priority, pr.ForwardingInfo.StatusCode), 293 Original: thisPr, 294 TTL: 1, 295 }) 296 } 297 return recs, nil 298 } 299 300 func (c *CloudflareApi) deletePageRule(recordID, domainID string) error { 301 endpoint := fmt.Sprintf(singlePageRuleURL, domainID, recordID) 302 req, err := http.NewRequest("DELETE", endpoint, nil) 303 if err != nil { 304 return err 305 } 306 c.setHeaders(req) 307 _, err = handleActionResponse(http.DefaultClient.Do(req)) 308 return err 309 } 310 311 func (c *CloudflareApi) updatePageRule(recordID, domainID string, target string) error { 312 if err := c.deletePageRule(recordID, domainID); err != nil { 313 return err 314 } 315 return c.createPageRule(domainID, target) 316 } 317 318 func (c *CloudflareApi) createPageRule(domainID string, target string) error { 319 endpoint := fmt.Sprintf(pageRulesURL, domainID) 320 return c.sendPageRule(endpoint, "POST", target) 321 } 322 323 func (c *CloudflareApi) sendPageRule(endpoint, method string, data string) error { 324 //from to priority code 325 parts := strings.Split(data, ",") 326 priority, _ := strconv.Atoi(parts[2]) 327 code, _ := strconv.Atoi(parts[3]) 328 fwdInfo := &pageRuleFwdInfo{ 329 StatusCode: code, 330 URL: parts[1], 331 } 332 dat, _ := json.Marshal(fwdInfo) 333 pr := &pageRule{ 334 Status: "active", 335 Priority: priority, 336 Targets: []pageRuleTarget{ 337 {Target: "url", Constraint: pageRuleConstraint{Operator: "matches", Value: parts[0]}}, 338 }, 339 Actions: []pageRuleAction{ 340 {ID: "forwarding_url", Value: json.RawMessage(dat)}, 341 }, 342 } 343 buf := &bytes.Buffer{} 344 enc := json.NewEncoder(buf) 345 if err := enc.Encode(pr); err != nil { 346 return err 347 } 348 req, err := http.NewRequest(method, endpoint, buf) 349 if err != nil { 350 return err 351 } 352 c.setHeaders(req) 353 _, err = handleActionResponse(http.DefaultClient.Do(req)) 354 return err 355 } 356 357 func stringifyErrors(errors []interface{}) string { 358 dat, err := json.Marshal(errors) 359 if err != nil { 360 return "???" 361 } 362 return string(dat) 363 } 364 365 type recordsResponse struct { 366 basicResponse 367 Result []*cfRecord `json:"result"` 368 ResultInfo pagingInfo `json:"result_info"` 369 } 370 371 type basicResponse struct { 372 Success bool `json:"success"` 373 Errors []interface{} `json:"errors"` 374 Messages []interface{} `json:"messages"` 375 Result struct { 376 ID string `json:"id"` 377 } `json:"result"` 378 } 379 380 type pageRuleResponse struct { 381 basicResponse 382 Result []*pageRule `json:"result"` 383 ResultInfo pagingInfo `json:"result_info"` 384 } 385 386 type pageRule struct { 387 ID string `json:"id,omitempty"` 388 Targets []pageRuleTarget `json:"targets"` 389 Actions []pageRuleAction `json:"actions"` 390 Priority int `json:"priority"` 391 Status string `json:"status"` 392 ModifiedOn time.Time `json:"modified_on,omitempty"` 393 CreatedOn time.Time `json:"created_on,omitempty"` 394 ForwardingInfo *pageRuleFwdInfo `json:"-"` 395 } 396 397 type pageRuleTarget struct { 398 Target string `json:"target"` 399 Constraint pageRuleConstraint `json:"constraint"` 400 } 401 402 type pageRuleConstraint struct { 403 Operator string `json:"operator"` 404 Value string `json:"value"` 405 } 406 407 type pageRuleAction struct { 408 ID string `json:"id"` 409 Value json.RawMessage `json:"value"` 410 } 411 412 type pageRuleFwdInfo struct { 413 URL string `json:"url"` 414 StatusCode int `json:"status_code"` 415 } 416 417 type zoneResponse struct { 418 basicResponse 419 Result []struct { 420 ID string `json:"id"` 421 Name string `json:"name"` 422 Nameservers []string `json:"name_servers"` 423 } `json:"result"` 424 ResultInfo pagingInfo `json:"result_info"` 425 } 426 427 type pagingInfo struct { 428 Page int `json:"page"` 429 PerPage int `json:"per_page"` 430 Count int `json:"count"` 431 TotalCount int `json:"total_count"` 432 }