dnscontrol/pkg/cloudflare-go/filter.go
Tom Limoncelli 7fd6a74e0c
CLOUDFLAREAPI: CF_REDIRECT/CF_TEMP_REDIRECT should dtrt using Single Redirects (#3002)
Co-authored-by: Josh Zhang <jzhang1@stackoverflow.com>
2024-06-18 17:38:50 -04:00

290 lines
8.4 KiB
Go

package cloudflare
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"github.com/goccy/go-json"
)
var ErrNotEnoughFilterIDsProvided = errors.New("at least one filter ID must be provided.")
// Filter holds the structure of the filter type.
type Filter struct {
ID string `json:"id,omitempty"`
Expression string `json:"expression"`
Paused bool `json:"paused"`
Description string `json:"description"`
// Property is mentioned in documentation however isn't populated in
// any of the API requests. For now, let's just omit it unless it's
// provided.
Ref string `json:"ref,omitempty"`
}
// FiltersDetailResponse is the API response that is returned
// for requesting all filters on a zone.
type FiltersDetailResponse struct {
Result []Filter `json:"result"`
ResultInfo `json:"result_info"`
Response
}
// FilterDetailResponse is the API response that is returned
// for requesting a single filter on a zone.
type FilterDetailResponse struct {
Result Filter `json:"result"`
ResultInfo `json:"result_info"`
Response
}
// FilterValidateExpression represents the JSON payload for checking
// an expression.
type FilterValidateExpression struct {
Expression string `json:"expression"`
}
// FilterValidateExpressionResponse represents the API response for
// checking the expression. It conforms to the JSON API approach however
// we don't need all of the fields exposed.
type FilterValidateExpressionResponse struct {
Success bool `json:"success"`
Errors []FilterValidationExpressionMessage `json:"errors"`
}
// FilterValidationExpressionMessage represents the API error message.
type FilterValidationExpressionMessage struct {
Message string `json:"message"`
}
// FilterCreateParams contains required and optional params
// for creating a filter.
type FilterCreateParams struct {
ID string `json:"id,omitempty"`
Expression string `json:"expression"`
Paused bool `json:"paused"`
Description string `json:"description"`
Ref string `json:"ref,omitempty"`
}
// FilterUpdateParams contains required and optional params
// for updating a filter.
type FilterUpdateParams struct {
ID string `json:"id"`
Expression string `json:"expression"`
Paused bool `json:"paused"`
Description string `json:"description"`
Ref string `json:"ref,omitempty"`
}
type FilterListParams struct {
ResultInfo
}
// Filter returns a single filter in a zone based on the filter ID.
//
// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/get/#get-by-filter-id
func (api *API) Filter(ctx context.Context, rc *ResourceContainer, filterID string) (Filter, error) {
uri := fmt.Sprintf("/zones/%s/filters/%s", rc.Identifier, filterID)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return Filter{}, err
}
var filterResponse FilterDetailResponse
err = json.Unmarshal(res, &filterResponse)
if err != nil {
return Filter{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return filterResponse.Result, nil
}
// Filters returns filters for a zone.
//
// Automatically paginates all results unless `params.PerPage` and `params.Page`
// is set.
//
// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/get/#get-all-filters
func (api *API) Filters(ctx context.Context, rc *ResourceContainer, params FilterListParams) ([]Filter, *ResultInfo, error) {
autoPaginate := true
if params.PerPage >= 1 || params.Page >= 1 {
autoPaginate = false
}
if params.PerPage < 1 {
params.PerPage = 50
}
if params.Page < 1 {
params.Page = 1
}
var filters []Filter
var fResponse FiltersDetailResponse
for {
fResponse = FiltersDetailResponse{}
uri := buildURI(fmt.Sprintf("/zones/%s/filters", rc.Identifier), params)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return []Filter{}, &ResultInfo{}, err
}
err = json.Unmarshal(res, &fResponse)
if err != nil {
return []Filter{}, &ResultInfo{}, fmt.Errorf("failed to unmarshal filters JSON data: %w", err)
}
filters = append(filters, fResponse.Result...)
params.ResultInfo = fResponse.ResultInfo.Next()
if params.ResultInfo.Done() || !autoPaginate {
break
}
}
return filters, &fResponse.ResultInfo, nil
}
// CreateFilters creates new filters.
//
// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/post/
func (api *API) CreateFilters(ctx context.Context, rc *ResourceContainer, params []FilterCreateParams) ([]Filter, error) {
uri := fmt.Sprintf("/zones/%s/filters", rc.Identifier)
res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
if err != nil {
return []Filter{}, err
}
var filtersResponse FiltersDetailResponse
err = json.Unmarshal(res, &filtersResponse)
if err != nil {
return []Filter{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return filtersResponse.Result, nil
}
// UpdateFilter updates a single filter.
//
// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/put/#update-a-single-filter
func (api *API) UpdateFilter(ctx context.Context, rc *ResourceContainer, params FilterUpdateParams) (Filter, error) {
if params.ID == "" {
return Filter{}, fmt.Errorf("filter ID cannot be empty")
}
uri := fmt.Sprintf("/zones/%s/filters/%s", rc.Identifier, params.ID)
res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
if err != nil {
return Filter{}, err
}
var filterResponse FilterDetailResponse
err = json.Unmarshal(res, &filterResponse)
if err != nil {
return Filter{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return filterResponse.Result, nil
}
// UpdateFilters updates many filters at once.
//
// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/put/#update-multiple-filters
func (api *API) UpdateFilters(ctx context.Context, rc *ResourceContainer, params []FilterUpdateParams) ([]Filter, error) {
for _, filter := range params {
if filter.ID == "" {
return []Filter{}, fmt.Errorf("filter ID cannot be empty")
}
}
uri := fmt.Sprintf("/zones/%s/filters", rc.Identifier)
res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
if err != nil {
return []Filter{}, err
}
var filtersResponse FiltersDetailResponse
err = json.Unmarshal(res, &filtersResponse)
if err != nil {
return []Filter{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
}
return filtersResponse.Result, nil
}
// DeleteFilter deletes a single filter.
//
// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/delete/#delete-a-single-filter
func (api *API) DeleteFilter(ctx context.Context, rc *ResourceContainer, filterID string) error {
if filterID == "" {
return fmt.Errorf("filter ID cannot be empty")
}
uri := fmt.Sprintf("/zones/%s/filters/%s", rc.Identifier, filterID)
_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
if err != nil {
return err
}
return nil
}
// DeleteFilters deletes multiple filters.
//
// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/delete/#delete-multiple-filters
func (api *API) DeleteFilters(ctx context.Context, rc *ResourceContainer, filterIDs []string) error {
if len(filterIDs) == 0 {
return ErrNotEnoughFilterIDsProvided
}
// Swap this to a typed struct and passing in all parameters together.
q := url.Values{}
for _, id := range filterIDs {
q.Add("id", id)
}
uri := (&url.URL{
Path: fmt.Sprintf("/zones/%s/filters", rc.Identifier),
RawQuery: q.Encode(),
}).String()
_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
if err != nil {
return err
}
return nil
}
// ValidateFilterExpression checks correctness of a filter expression.
//
// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/validation/
func (api *API) ValidateFilterExpression(ctx context.Context, expression string) error {
expressionPayload := FilterValidateExpression{Expression: expression}
_, err := api.makeRequestContext(ctx, http.MethodPost, "/filters/validate-expr", expressionPayload)
if err != nil {
var filterValidationResponse FilterValidateExpressionResponse
jsonErr := json.Unmarshal([]byte(err.Error()), &filterValidationResponse)
if jsonErr != nil {
return fmt.Errorf(errUnmarshalError+": %w", jsonErr)
}
if !filterValidationResponse.Success {
// Unsure why but the API returns `errors` as an array but it only
// ever shows the issue with one problem at a time ¯\_(ツ)_/¯
return fmt.Errorf(filterValidationResponse.Errors[0].Message)
}
}
return nil
}