1package cloudflare 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "net/url" 9 "strconv" 10 "strings" 11 12 "github.com/pkg/errors" 13) 14 15// Filter holds the structure of the filter type. 16type Filter struct { 17 ID string `json:"id,omitempty"` 18 Expression string `json:"expression"` 19 Paused bool `json:"paused"` 20 Description string `json:"description"` 21 22 // Property is mentioned in documentation however isn't populated in 23 // any of the API requests. For now, let's just omit it unless it's 24 // provided. 25 Ref string `json:"ref,omitempty"` 26} 27 28// FiltersDetailResponse is the API response that is returned 29// for requesting all filters on a zone. 30type FiltersDetailResponse struct { 31 Result []Filter `json:"result"` 32 ResultInfo `json:"result_info"` 33 Response 34} 35 36// FilterDetailResponse is the API response that is returned 37// for requesting a single filter on a zone. 38type FilterDetailResponse struct { 39 Result Filter `json:"result"` 40 ResultInfo `json:"result_info"` 41 Response 42} 43 44// FilterValidateExpression represents the JSON payload for checking 45// an expression. 46type FilterValidateExpression struct { 47 Expression string `json:"expression"` 48} 49 50// FilterValidateExpressionResponse represents the API response for 51// checking the expression. It conforms to the JSON API approach however 52// we don't need all of the fields exposed. 53type FilterValidateExpressionResponse struct { 54 Success bool `json:"success"` 55 Errors []FilterValidationExpressionMessage `json:"errors"` 56} 57 58// FilterValidationExpressionMessage represents the API error message. 59type FilterValidationExpressionMessage struct { 60 Message string `json:"message"` 61} 62 63// Filter returns a single filter in a zone based on the filter ID. 64// 65// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/get/#get-by-filter-id 66func (api *API) Filter(ctx context.Context, zoneID, filterID string) (Filter, error) { 67 uri := fmt.Sprintf("/zones/%s/filters/%s", zoneID, filterID) 68 69 res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) 70 if err != nil { 71 return Filter{}, err 72 } 73 74 var filterResponse FilterDetailResponse 75 err = json.Unmarshal(res, &filterResponse) 76 if err != nil { 77 return Filter{}, errors.Wrap(err, errUnmarshalError) 78 } 79 80 return filterResponse.Result, nil 81} 82 83// Filters returns all filters for a zone. 84// 85// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/get/#get-all-filters 86func (api *API) Filters(ctx context.Context, zoneID string, pageOpts PaginationOptions) ([]Filter, error) { 87 uri := fmt.Sprintf("/zones/%s/filters", zoneID) 88 v := url.Values{} 89 90 if pageOpts.PerPage > 0 { 91 v.Set("per_page", strconv.Itoa(pageOpts.PerPage)) 92 } 93 94 if pageOpts.Page > 0 { 95 v.Set("page", strconv.Itoa(pageOpts.Page)) 96 } 97 98 if len(v) > 0 { 99 uri = fmt.Sprintf("%s?%s", uri, v.Encode()) 100 } 101 102 res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil) 103 if err != nil { 104 return []Filter{}, err 105 } 106 107 var filtersResponse FiltersDetailResponse 108 err = json.Unmarshal(res, &filtersResponse) 109 if err != nil { 110 return []Filter{}, errors.Wrap(err, errUnmarshalError) 111 } 112 113 return filtersResponse.Result, nil 114} 115 116// CreateFilters creates new filters. 117// 118// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/post/ 119func (api *API) CreateFilters(ctx context.Context, zoneID string, filters []Filter) ([]Filter, error) { 120 uri := fmt.Sprintf("/zones/%s/filters", zoneID) 121 122 res, err := api.makeRequestContext(ctx, http.MethodPost, uri, filters) 123 if err != nil { 124 return []Filter{}, err 125 } 126 127 var filtersResponse FiltersDetailResponse 128 err = json.Unmarshal(res, &filtersResponse) 129 if err != nil { 130 return []Filter{}, errors.Wrap(err, errUnmarshalError) 131 } 132 133 return filtersResponse.Result, nil 134} 135 136// UpdateFilter updates a single filter. 137// 138// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/put/#update-a-single-filter 139func (api *API) UpdateFilter(ctx context.Context, zoneID string, filter Filter) (Filter, error) { 140 if filter.ID == "" { 141 return Filter{}, errors.Errorf("filter ID cannot be empty") 142 } 143 144 uri := fmt.Sprintf("/zones/%s/filters/%s", zoneID, filter.ID) 145 146 res, err := api.makeRequestContext(ctx, http.MethodPut, uri, filter) 147 if err != nil { 148 return Filter{}, err 149 } 150 151 var filterResponse FilterDetailResponse 152 err = json.Unmarshal(res, &filterResponse) 153 if err != nil { 154 return Filter{}, errors.Wrap(err, errUnmarshalError) 155 } 156 157 return filterResponse.Result, nil 158} 159 160// UpdateFilters updates many filters at once. 161// 162// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/put/#update-multiple-filters 163func (api *API) UpdateFilters(ctx context.Context, zoneID string, filters []Filter) ([]Filter, error) { 164 for _, filter := range filters { 165 if filter.ID == "" { 166 return []Filter{}, errors.Errorf("filter ID cannot be empty") 167 } 168 } 169 170 uri := fmt.Sprintf("/zones/%s/filters", zoneID) 171 172 res, err := api.makeRequestContext(ctx, http.MethodPut, uri, filters) 173 if err != nil { 174 return []Filter{}, err 175 } 176 177 var filtersResponse FiltersDetailResponse 178 err = json.Unmarshal(res, &filtersResponse) 179 if err != nil { 180 return []Filter{}, errors.Wrap(err, errUnmarshalError) 181 } 182 183 return filtersResponse.Result, nil 184} 185 186// DeleteFilter deletes a single filter. 187// 188// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/delete/#delete-a-single-filter 189func (api *API) DeleteFilter(ctx context.Context, zoneID, filterID string) error { 190 if filterID == "" { 191 return errors.Errorf("filter ID cannot be empty") 192 } 193 194 uri := fmt.Sprintf("/zones/%s/filters/%s", zoneID, filterID) 195 196 _, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) 197 if err != nil { 198 return err 199 } 200 201 return nil 202} 203 204// DeleteFilters deletes multiple filters. 205// 206// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/delete/#delete-multiple-filters 207func (api *API) DeleteFilters(ctx context.Context, zoneID string, filterIDs []string) error { 208 ids := strings.Join(filterIDs, ",") 209 uri := fmt.Sprintf("/zones/%s/filters?id=%s", zoneID, ids) 210 211 _, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil) 212 if err != nil { 213 return err 214 } 215 216 return nil 217} 218 219// ValidateFilterExpression checks correctness of a filter expression. 220// 221// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/validation/ 222func (api *API) ValidateFilterExpression(ctx context.Context, expression string) error { 223 expressionPayload := FilterValidateExpression{Expression: expression} 224 225 _, err := api.makeRequestContext(ctx, http.MethodPost, "/filters/validate-expr", expressionPayload) 226 if err != nil { 227 var filterValidationResponse FilterValidateExpressionResponse 228 229 jsonErr := json.Unmarshal([]byte(err.Error()), &filterValidationResponse) 230 if jsonErr != nil { 231 return errors.Wrap(jsonErr, errUnmarshalError) 232 } 233 234 if !filterValidationResponse.Success { 235 // Unsure why but the API returns `errors` as an array but it only 236 // ever shows the issue with one problem at a time ¯\_(ツ)_/¯ 237 return errors.Errorf(filterValidationResponse.Errors[0].Message) 238 } 239 } 240 241 return nil 242} 243