// package cf implements Cloudflare API client package cf import ( "encoding/json" "fmt" "io" "net/http" "time" ) type Client struct { APIKey string APIURL string } func NewClient(apiKey string) *Client { return &Client{APIKey: apiKey, APIURL: "https://api.cloudflare.com/client/v4"} } // Record represents a single DNS record in cf type Record struct { ID string `json:"id"` ZoneID string `json:"zone_id"` ZoneName string `json:"zone_name"` Name string `json:"name"` Type string `json:"type"` Content string `json:"content"` Proxiable bool `json:"proxiable"` Proxied bool `json:"proxied"` TTL int `json:"ttl"` Comment string `json:"comment"` Tags []string `json:"tags"` CreatedOn time.Time `json:"created_on"` ModifiedOn time.Time `json:"modified_on"` NumberQueries int } // RecordResult represents the HTTP response body of an API call to get DNS Records type RecordResult struct { Result []Record `json:"result"` } // Zone represents a single DNS zone in cf type Zone struct { ID string `json:"id"` Name string `json:"name"` } // ZoneResult represents the HTTP response body of an API call to get DNS Zones type ZoneResult struct { Result []Zone `json:"result"` } // Analytic represents the DNS analytic data returned from cf type Analytic struct { Result struct { Rows int `json:"rows"` Data []struct { Dimensions []string `json:"dimensions"` Metrics []int `json:"metrics"` } `json:"data"` DataLag int `json:"data_lag"` Min struct { } `json:"min"` Max struct { } `json:"max"` Totals struct { QueryCount int `json:"queryCount"` } `json:"totals"` Query struct { Dimensions []any `json:"dimensions"` Metrics []string `json:"metrics"` Since time.Time `json:"since"` Until time.Time `json:"until"` Limit int `json:"limit"` } `json:"query"` } `json:"result"` } // getZones provides a slice of all zones in the cf account func (c *Client) GetZones() ([]Zone, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/zones", c.APIURL), nil) if err != nil { return nil, fmt.Errorf("failed to create new http request: %v", err) } req.Header.Add("Content-Type", "application/json") req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.APIKey)) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute http request: %v", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("http response has status '%d' but expected '%d'", resp.StatusCode, http.StatusOK) } b, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read http response body: %v", err) } zoneResult := &ZoneResult{} err = json.Unmarshal(b, zoneResult) if err != nil { return nil, fmt.Errorf("failed to unmarshal response body to struct: %v", err) } return zoneResult.Result, nil } // getRecords provides a slice of all records in the cf zone provided func (c *Client) GetRecords(zone Zone) ([]Record, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/zones/%s/dns_records", c.APIURL, zone.ID), nil) if err != nil { return nil, fmt.Errorf("failed to create new http request: %v", err) } req.Header.Add("Content-Type", "application/json") req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.APIKey)) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute http request: %v", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("http response has status '%d' but expected '%d'", resp.StatusCode, http.StatusOK) } b, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read http response body: %v", err) } recordResult := &RecordResult{} err = json.Unmarshal(b, recordResult) if err != nil { return nil, fmt.Errorf("failed to unmarshal response body to struct: %v", err) } return recordResult.Result, nil } // getRecordAnalytic provides a pointer to an Analytic for the given func (c *Client) GetRecordAnalytic(record Record) (*Analytic, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/zones/%s/dns_analytics/report", c.APIURL, record.ZoneID), nil) if err != nil { return nil, fmt.Errorf("failed to create new http request: %v", err) } req.Header.Add("Content-Type", "application/json") req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.APIKey)) q := req.URL.Query() q.Add("filters", fmt.Sprintf("queryName==%s", record.Name)) /* TBD: Why does this give HTTP 403? q.Add("filters", fmt.Sprintf("queryName==%s,queryType==%s", record.Name, record.Type)) */ req.URL.RawQuery = q.Encode() client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute http request: %v", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("http response has status '%d' but expected '%d'", resp.StatusCode, http.StatusOK) } b, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read http response body: %v", err) } analytic := &Analytic{} err = json.Unmarshal(b, analytic) if err != nil { return nil, fmt.Errorf("failed to unmarshal response body to struct: %v", err) } return analytic, nil } // getRecordAnalytic provides a pointer to an Analytic for the given func (c *Client) GetZoneAnalytic(zone Zone) (*Analytic, error) { req, err := http.NewRequest("GET", fmt.Sprintf("%s/zones/%s/dns_analytics/report", c.APIURL, zone.ID), nil) if err != nil { return nil, fmt.Errorf("failed to create new http request: %v", err) } req.Header.Add("Content-Type", "application/json") req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.APIKey)) q := req.URL.Query() q.Add("metrics", "queryCount") q.Add("dimensions", "queryName") req.URL.RawQuery = q.Encode() client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute http request: %v", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("http response has status '%d' but expected '%d'", resp.StatusCode, http.StatusOK) } b, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read http response body: %v", err) } analytic := &Analytic{} err = json.Unmarshal(b, analytic) if err != nil { return nil, fmt.Errorf("failed to unmarshal response body to struct: %v", err) } return analytic, nil }