diff --git a/cf/cf.go b/cf/cf.go new file mode 100644 index 0000000..9d9ea35 --- /dev/null +++ b/cf/cf.go @@ -0,0 +1,186 @@ +// 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 { + 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 400? + 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 +} diff --git a/main.go b/main.go index a17358d..e80efc6 100644 --- a/main.go +++ b/main.go @@ -3,95 +3,50 @@ package main import ( "bytes" "encoding/csv" - "encoding/json" + "flag" "fmt" "io" "log" - "net/http" - "os" "sort" "strconv" "time" + + "code.stevenpolley.net/steven/cfcleaner/cf" ) -const apiURL = "https://api.cloudflare.com/client/v4" +func init() { -var apiKey string - -type Zone struct { - ID string `json:"id"` - Name string `json:"name"` -} - -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 -} - -type ZoneResult struct { - Result []Zone `json:"result"` -} - -type RecordResult struct { - Result []Record `json:"result"` -} - -type Analytic struct { - Result struct { - Rows int `json:"rows"` - Data []struct { - 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"` } func main() { - apiKey = os.Getenv("cfapikey") - zones, err := getZones() + cfAPIKey := flag.String("cfapikey", "", "in cf, my profile -> API Tokens -> Create Token") + flag.Parse() + + if *cfAPIKey == "" { + fmt.Println("you must specify an API key with the -cfapikey= parameter") + return + } + + cfClient := cf.NewClient(*cfAPIKey) + + zones, err := cfClient.GetZones() if err != nil { log.Fatalf("failed to get zones: %v", err) } - outRows := make([]Record, 0) + outRows := make([]cf.Record, 0) for _, zone := range zones { log.Printf("processing zone '%s' with ID '%s'", zone.Name, zone.ID) - records, err := getRecords(zone) + records, err := cfClient.GetRecords(zone) if err != nil { log.Printf("failed to get records for zone '%s' with ID '%s': %v", zone.Name, zone.ID, err) } for _, record := range records { - analytic, err := getRecordAnalytic(zone, record) + analytic, err := cfClient.GetRecordAnalytic(record) if err != nil { log.Printf("failed to get record report for record '%s' in zone '%s' with ID '%s': %v", record.Name, zone.Name, zone.ID, err) continue @@ -122,109 +77,3 @@ func main() { fmt.Println(string(b)) } - -func getZones() ([]Zone, error) { - req, err := http.NewRequest("GET", fmt.Sprintf("%s/zones", 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("X-Auth-Email", apiKey) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", 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 -} - -func getRecords(zone Zone) ([]Record, error) { - req, err := http.NewRequest("GET", fmt.Sprintf("%s/zones/%s/dns_records", 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("X-Auth-Email", apiKey) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", 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 -} - -func getRecordAnalytic(zone Zone, record Record) (*Analytic, error) { - req, err := http.NewRequest("GET", fmt.Sprintf("%s/zones/%s/dns_analytics/report", 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("X-Auth-Email", apiKey) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiKey)) - - q := req.URL.Query() - q.Add("filters", fmt.Sprintf("queryName==%s", record.Name)) - //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 - -}