1
0
mirror of https://github.com/taigrr/godns synced 2025-01-18 04:03:25 -08:00
godns/handler/cloudflare/cloudflare_handler.go
Marvin c720f7bca1
Fix API token permission issue with CloudFlare
When querying CloudFlare for all zones via the API using a restricted API token, CloudFlare responds, that that particular token needs the "com.cloudflare.api.account.zone.list" permission, in order to successfully retrieve all zones.
Adding the parameter "name" with the value of the given domain works around that limitation, as then the API token only needs access to that particular domain.
2020-11-06 20:35:40 +01:00

264 lines
6.7 KiB
Go

package cloudflare
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"runtime/debug"
"strings"
"time"
"github.com/TimothyYe/godns"
)
// Handler struct definition
type Handler struct {
Configuration *godns.Settings
API string
}
// DNSRecordResponse struct
type DNSRecordResponse struct {
Records []DNSRecord `json:"result"`
Success bool `json:"success"`
}
// DNSRecordUpdateResponse struct
type DNSRecordUpdateResponse struct {
Record DNSRecord `json:"result"`
Success bool `json:"success"`
}
// DNSRecord for Cloudflare API
type DNSRecord struct {
ID string `json:"id"`
IP string `json:"content"`
Name string `json:"name"`
Proxied bool `json:"proxied"`
Type string `json:"type"`
ZoneID string `json:"zone_id"`
TTL int32 `json:"ttl"`
}
// SetIP updates DNSRecord.IP
func (r *DNSRecord) SetIP(ip string) {
r.IP = ip
}
// ZoneResponse is a wrapper for Zones
type ZoneResponse struct {
Zones []Zone `json:"result"`
Success bool `json:"success"`
}
// Zone object with id and name
type Zone struct {
ID string `json:"id"`
Name string `json:"name"`
}
// SetConfiguration pass dns settings and store it to handler instance
func (handler *Handler) SetConfiguration(conf *godns.Settings) {
handler.Configuration = conf
handler.API = "https://api.cloudflare.com/client/v4"
}
// DomainLoop the main logic loop
func (handler *Handler) DomainLoop(domain *godns.Domain, panicChan chan<- godns.Domain) {
defer func() {
if err := recover(); err != nil {
log.Printf("Recovered in %v: %v\n", err, debug.Stack())
panicChan <- *domain
}
}()
var lastIP string
looping := false
for {
if looping {
// Sleep with interval
log.Printf("Going to sleep, will start next checking in %d seconds...\r\n", handler.Configuration.Interval)
time.Sleep(time.Second * time.Duration(handler.Configuration.Interval))
}
looping = true
currentIP, err := godns.GetCurrentIP(handler.Configuration)
if err != nil {
log.Println("Error in GetCurrentIP:", err)
continue
}
log.Println("Current IP is:", currentIP)
//check against locally cached IP, if no change, skip update
if currentIP == lastIP {
log.Printf("IP is the same as cached one. Skip update.\n")
} else {
log.Println("Checking IP for domain", domain.DomainName)
zoneID := handler.getZone(domain.DomainName)
if zoneID != "" {
records := handler.getDNSRecords(zoneID)
// update records
for _, rec := range records {
if !recordTracked(domain, &rec) {
log.Println("Skiping record:", rec.Name)
continue
}
if rec.IP != currentIP {
log.Printf("IP mismatch: Current(%+v) vs Cloudflare(%+v)\r\n", currentIP, rec.IP)
lastIP = handler.updateRecord(rec, currentIP)
// Send notification
if err := godns.SendNotify(handler.Configuration, rec.Name, currentIP); err != nil {
log.Println("Failed to send notification")
}
} else {
log.Printf("Record OK: %+v - %+v\r\n", rec.Name, rec.IP)
}
}
} else {
log.Println("Failed to find zone for domain:", domain.DomainName)
}
}
}
}
// Check if record is present in domain conf
func recordTracked(domain *godns.Domain, record *DNSRecord) bool {
for _, subDomain := range domain.SubDomains {
sd := fmt.Sprintf("%s.%s", subDomain, domain.DomainName)
if record.Name == sd {
return true
}
}
return false
}
// Create a new request with auth in place and optional proxy
func (handler *Handler) newRequest(method, url string, body io.Reader) (*http.Request, *http.Client) {
client := godns.GetHttpClient(handler.Configuration, handler.Configuration.UseProxy)
if client == nil {
log.Println("cannot create HTTP client")
}
req, _ := http.NewRequest(method, handler.API+url, body)
req.Header.Set("Content-Type", "application/json")
if handler.Configuration.Email != "" && handler.Configuration.Password != "" {
req.Header.Set("X-Auth-Email", handler.Configuration.Email)
req.Header.Set("X-Auth-Key", handler.Configuration.Password)
} else if handler.Configuration.LoginToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", handler.Configuration.LoginToken))
}
return req, client
}
// Find the correct zone via domain name
func (handler *Handler) getZone(domain string) string {
var z ZoneResponse
req, client := handler.newRequest("GET", fmt.Sprintf("/zones?name=%s", domain), nil)
resp, err := client.Do(req)
if err != nil {
log.Println("Request error:", err.Error())
return ""
}
body, _ := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &z)
if err != nil {
log.Printf("Decoder error: %+v\n", err)
log.Printf("Response body: %+v\n", string(body))
return ""
}
if z.Success != true {
log.Printf("Response failed: %+v\n", string(body))
return ""
}
for _, zone := range z.Zones {
if zone.Name == domain {
return zone.ID
}
}
return ""
}
// Get all DNS A records for a zone
func (handler *Handler) getDNSRecords(zoneID string) []DNSRecord {
var empty []DNSRecord
var r DNSRecordResponse
var recordType string
if handler.Configuration.IPType == "" || strings.ToUpper(handler.Configuration.IPType) == godns.IPV4 {
recordType = "A"
} else if strings.ToUpper(handler.Configuration.IPType) == godns.IPV6 {
recordType = "AAAA"
}
log.Println("Querying records with type:", recordType)
req, client := handler.newRequest("GET", fmt.Sprintf("/zones/"+zoneID+"/dns_records?type=%s&page=1&per_page=500", recordType), nil)
resp, err := client.Do(req)
if err != nil {
log.Println("Request error:", err.Error())
return empty
}
body, _ := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &r)
if err != nil {
log.Printf("Decoder error: %+v\n", err)
log.Printf("Response body: %+v\n", string(body))
return empty
}
if r.Success != true {
body, _ := ioutil.ReadAll(resp.Body)
log.Printf("Response failed: %+v\n", string(body))
return empty
}
return r.Records
}
// Update DNS A Record with new IP
func (handler *Handler) updateRecord(record DNSRecord, newIP string) string {
var r DNSRecordUpdateResponse
record.SetIP(newIP)
var lastIP string
j, _ := json.Marshal(record)
req, client := handler.newRequest("PUT",
"/zones/"+record.ZoneID+"/dns_records/"+record.ID,
bytes.NewBuffer(j),
)
resp, err := client.Do(req)
if err != nil {
log.Println("Request error:", err.Error())
return ""
}
body, _ := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &r)
if err != nil {
log.Printf("Decoder error: %+v\n", err)
log.Printf("Response body: %+v\n", string(body))
return ""
}
if r.Success != true {
body, _ := ioutil.ReadAll(resp.Body)
log.Printf("Response failed: %+v\n", string(body))
} else {
log.Printf("Record updated: %+v - %+v", record.Name, record.IP)
lastIP = record.IP
}
return lastIP
}