1package cloudflare
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"net/http"
8	"time"
9
10	"github.com/pkg/errors"
11)
12
13const (
14	// IPListTypeIP specifies a list containing IP addresses
15	IPListTypeIP = "ip"
16)
17
18// IPListBulkOperation contains information about a Bulk Operation
19type IPListBulkOperation struct {
20	ID        string     `json:"id"`
21	Status    string     `json:"status"`
22	Error     string     `json:"error"`
23	Completed *time.Time `json:"completed"`
24}
25
26// IPList contains information about an IP List
27type IPList struct {
28	ID                    string     `json:"id"`
29	Name                  string     `json:"name"`
30	Description           string     `json:"description"`
31	Kind                  string     `json:"kind"`
32	NumItems              int        `json:"num_items"`
33	NumReferencingFilters int        `json:"num_referencing_filters"`
34	CreatedOn             *time.Time `json:"created_on"`
35	ModifiedOn            *time.Time `json:"modified_on"`
36}
37
38// IPListItem contains information about a single IP List Item
39type IPListItem struct {
40	ID         string     `json:"id"`
41	IP         string     `json:"ip"`
42	Comment    string     `json:"comment"`
43	CreatedOn  *time.Time `json:"created_on"`
44	ModifiedOn *time.Time `json:"modified_on"`
45}
46
47// IPListCreateRequest contains data for a new IP List
48type IPListCreateRequest struct {
49	Name        string `json:"name"`
50	Description string `json:"description"`
51	Kind        string `json:"kind"`
52}
53
54// IPListItemCreateRequest contains data for a new IP List Item
55type IPListItemCreateRequest struct {
56	IP      string `json:"ip"`
57	Comment string `json:"comment"`
58}
59
60// IPListItemDeleteRequest wraps IP List Items that shall be deleted
61type IPListItemDeleteRequest struct {
62	Items []IPListItemDeleteItemRequest `json:"items"`
63}
64
65// IPListItemDeleteItemRequest contains single IP List Items that shall be deleted
66type IPListItemDeleteItemRequest struct {
67	ID string `json:"id"`
68}
69
70// IPListUpdateRequest contains data for an IP List update
71type IPListUpdateRequest struct {
72	Description string `json:"description"`
73}
74
75// IPListResponse contains a single IP List
76type IPListResponse struct {
77	Response
78	Result IPList `json:"result"`
79}
80
81// IPListItemCreateResponse contains information about the creation of an IP List Item
82type IPListItemCreateResponse struct {
83	Response
84	Result struct {
85		OperationID string `json:"operation_id"`
86	} `json:"result"`
87}
88
89// IPListListResponse contains a slice of IP Lists
90type IPListListResponse struct {
91	Response
92	Result []IPList `json:"result"`
93}
94
95// IPListBulkOperationResponse contains information about a Bulk Operation
96type IPListBulkOperationResponse struct {
97	Response
98	Result IPListBulkOperation `json:"result"`
99}
100
101// IPListDeleteResponse contains information about the deletion of an IP List
102type IPListDeleteResponse struct {
103	Response
104	Result struct {
105		ID string `json:"id"`
106	} `json:"result"`
107}
108
109// IPListItemsListResponse contains information about IP List Items
110type IPListItemsListResponse struct {
111	Response
112	ResultInfo `json:"result_info"`
113	Result     []IPListItem `json:"result"`
114}
115
116// IPListItemDeleteResponse contains information about the deletion of an IP List Item
117type IPListItemDeleteResponse struct {
118	Response
119	Result struct {
120		OperationID string `json:"operation_id"`
121	} `json:"result"`
122}
123
124// IPListItemsGetResponse contains information about a single IP List Item
125type IPListItemsGetResponse struct {
126	Response
127	Result IPListItem `json:"result"`
128}
129
130// ListIPLists lists all IP Lists
131//
132// API reference: https://api.cloudflare.com/#rules-lists-list-lists
133func (api *API) ListIPLists(ctx context.Context) ([]IPList, error) {
134	uri := fmt.Sprintf("/accounts/%s/rules/lists", api.AccountID)
135	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
136	if err != nil {
137		return []IPList{}, err
138	}
139
140	result := IPListListResponse{}
141	if err := json.Unmarshal(res, &result); err != nil {
142		return []IPList{}, errors.Wrap(err, errUnmarshalError)
143	}
144
145	return result.Result, nil
146}
147
148// CreateIPList creates a new IP List
149//
150// API reference: https://api.cloudflare.com/#rules-lists-create-list
151func (api *API) CreateIPList(ctx context.Context, name string, description string, kind string) (IPList,
152	error) {
153	uri := fmt.Sprintf("/accounts/%s/rules/lists", api.AccountID)
154	res, err := api.makeRequestContext(ctx, http.MethodPost, uri,
155		IPListCreateRequest{Name: name, Description: description, Kind: kind})
156	if err != nil {
157		return IPList{}, err
158	}
159
160	result := IPListResponse{}
161	if err := json.Unmarshal(res, &result); err != nil {
162		return IPList{}, errors.Wrap(err, errUnmarshalError)
163	}
164
165	return result.Result, nil
166}
167
168// GetIPList returns a single IP List
169//
170// API reference: https://api.cloudflare.com/#rules-lists-get-list
171func (api *API) GetIPList(ctx context.Context, id string) (IPList, error) {
172	uri := fmt.Sprintf("/accounts/%s/rules/lists/%s", api.AccountID, id)
173	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
174	if err != nil {
175		return IPList{}, err
176	}
177
178	result := IPListResponse{}
179	if err := json.Unmarshal(res, &result); err != nil {
180		return IPList{}, errors.Wrap(err, errUnmarshalError)
181	}
182
183	return result.Result, nil
184}
185
186// UpdateIPList updates the description of an existing IP List
187//
188// API reference: https://api.cloudflare.com/#rules-lists-update-list
189func (api *API) UpdateIPList(ctx context.Context, id string, description string) (IPList, error) {
190	uri := fmt.Sprintf("/accounts/%s/rules/lists/%s", api.AccountID, id)
191	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, IPListUpdateRequest{Description: description})
192	if err != nil {
193		return IPList{}, err
194	}
195
196	result := IPListResponse{}
197	if err := json.Unmarshal(res, &result); err != nil {
198		return IPList{}, errors.Wrap(err, errUnmarshalError)
199	}
200
201	return result.Result, nil
202}
203
204// DeleteIPList deletes an IP List
205//
206// API reference: https://api.cloudflare.com/#rules-lists-delete-list
207func (api *API) DeleteIPList(ctx context.Context, id string) (IPListDeleteResponse, error) {
208	uri := fmt.Sprintf("/accounts/%s/rules/lists/%s", api.AccountID, id)
209	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
210	if err != nil {
211		return IPListDeleteResponse{}, err
212	}
213
214	result := IPListDeleteResponse{}
215	if err := json.Unmarshal(res, &result); err != nil {
216		return IPListDeleteResponse{}, errors.Wrap(err, errUnmarshalError)
217	}
218
219	return result, nil
220}
221
222// ListIPListItems returns a list with all items in an IP List
223//
224// API reference: https://api.cloudflare.com/#rules-lists-list-list-items
225func (api *API) ListIPListItems(ctx context.Context, id string) ([]IPListItem, error) {
226	var list []IPListItem
227	var cursor string
228	var cursorQuery string
229
230	for {
231		if len(cursor) > 0 {
232			cursorQuery = fmt.Sprintf("?cursor=%s", cursor)
233		}
234		uri := fmt.Sprintf("/accounts/%s/rules/lists/%s/items%s", api.AccountID, id, cursorQuery)
235		res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
236		if err != nil {
237			return []IPListItem{}, err
238		}
239
240		result := IPListItemsListResponse{}
241		if err := json.Unmarshal(res, &result); err != nil {
242			return []IPListItem{}, errors.Wrap(err, errUnmarshalError)
243		}
244
245		list = append(list, result.Result...)
246		if cursor = result.ResultInfo.Cursors.After; cursor == "" {
247			break
248		}
249	}
250
251	return list, nil
252}
253
254// CreateIPListItemAsync creates a new IP List Item asynchronously. Users have to poll the operation status by
255// using the operation_id returned by this function.
256//
257// API reference: https://api.cloudflare.com/#rules-lists-create-list-items
258func (api *API) CreateIPListItemAsync(ctx context.Context, id, ip, comment string) (IPListItemCreateResponse, error) {
259	uri := fmt.Sprintf("/accounts/%s/rules/lists/%s/items", api.AccountID, id)
260	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, []IPListItemCreateRequest{{IP: ip, Comment: comment}})
261	if err != nil {
262		return IPListItemCreateResponse{}, err
263	}
264
265	result := IPListItemCreateResponse{}
266	if err := json.Unmarshal(res, &result); err != nil {
267		return IPListItemCreateResponse{}, errors.Wrap(err, errUnmarshalError)
268	}
269
270	return result, nil
271}
272
273// CreateIPListItem creates a new IP List Item synchronously and returns the current set of IP List Items
274func (api *API) CreateIPListItem(ctx context.Context, id, ip, comment string) ([]IPListItem, error) {
275	result, err := api.CreateIPListItemAsync(ctx, id, ip, comment)
276
277	if err != nil {
278		return []IPListItem{}, err
279	}
280
281	err = api.pollIPListBulkOperation(ctx, result.Result.OperationID)
282	if err != nil {
283		return []IPListItem{}, err
284	}
285
286	return api.ListIPListItems(ctx, id)
287}
288
289// CreateIPListItemsAsync bulk creates many IP List Items asynchronously. Users have to poll the operation status by
290// using the operation_id returned by this function.
291//
292// API reference: https://api.cloudflare.com/#rules-lists-create-list-items
293func (api *API) CreateIPListItemsAsync(ctx context.Context, id string, items []IPListItemCreateRequest) (
294	IPListItemCreateResponse, error) {
295	uri := fmt.Sprintf("/accounts/%s/rules/lists/%s/items", api.AccountID, id)
296	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, items)
297	if err != nil {
298		return IPListItemCreateResponse{}, err
299	}
300
301	result := IPListItemCreateResponse{}
302	if err := json.Unmarshal(res, &result); err != nil {
303		return IPListItemCreateResponse{}, errors.Wrap(err, errUnmarshalError)
304	}
305
306	return result, nil
307}
308
309// CreateIPListItems bulk creates many IP List Items synchronously and returns the current set of IP List Items
310func (api *API) CreateIPListItems(ctx context.Context, id string, items []IPListItemCreateRequest) (
311	[]IPListItem, error) {
312	result, err := api.CreateIPListItemsAsync(ctx, id, items)
313	if err != nil {
314		return []IPListItem{}, err
315	}
316
317	err = api.pollIPListBulkOperation(ctx, result.Result.OperationID)
318	if err != nil {
319		return []IPListItem{}, err
320	}
321
322	return api.ListIPListItems(ctx, id)
323}
324
325// ReplaceIPListItemsAsync replaces all IP List Items asynchronously. Users have to poll the operation status by
326// using the operation_id returned by this function.
327//
328// API reference: https://api.cloudflare.com/#rules-lists-replace-list-items
329func (api *API) ReplaceIPListItemsAsync(ctx context.Context, id string, items []IPListItemCreateRequest) (
330	IPListItemCreateResponse, error) {
331	uri := fmt.Sprintf("/accounts/%s/rules/lists/%s/items", api.AccountID, id)
332	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, items)
333	if err != nil {
334		return IPListItemCreateResponse{}, err
335	}
336
337	result := IPListItemCreateResponse{}
338	if err := json.Unmarshal(res, &result); err != nil {
339		return IPListItemCreateResponse{}, errors.Wrap(err, errUnmarshalError)
340	}
341
342	return result, nil
343}
344
345// ReplaceIPListItems replaces all IP List Items synchronously and returns the current set of IP List Items
346func (api *API) ReplaceIPListItems(ctx context.Context, id string, items []IPListItemCreateRequest) (
347	[]IPListItem, error) {
348	result, err := api.ReplaceIPListItemsAsync(ctx, id, items)
349	if err != nil {
350		return []IPListItem{}, err
351	}
352
353	err = api.pollIPListBulkOperation(ctx, result.Result.OperationID)
354	if err != nil {
355		return []IPListItem{}, err
356	}
357
358	return api.ListIPListItems(ctx, id)
359}
360
361// DeleteIPListItemsAsync removes specific Items of an IP List by their ID asynchronously. Users have to poll the
362// operation status by using the operation_id returned by this function.
363//
364// API reference: https://api.cloudflare.com/#rules-lists-delete-list-items
365func (api *API) DeleteIPListItemsAsync(ctx context.Context, id string, items IPListItemDeleteRequest) (
366	IPListItemDeleteResponse, error) {
367	uri := fmt.Sprintf("/accounts/%s/rules/lists/%s/items", api.AccountID, id)
368	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, items)
369	if err != nil {
370		return IPListItemDeleteResponse{}, err
371	}
372
373	result := IPListItemDeleteResponse{}
374	if err := json.Unmarshal(res, &result); err != nil {
375		return IPListItemDeleteResponse{}, errors.Wrap(err, errUnmarshalError)
376	}
377
378	return result, nil
379}
380
381// DeleteIPListItems removes specific Items of an IP List by their ID synchronously and returns the current set
382// of IP List Items
383func (api *API) DeleteIPListItems(ctx context.Context, id string, items IPListItemDeleteRequest) (
384	[]IPListItem, error) {
385	result, err := api.DeleteIPListItemsAsync(ctx, id, items)
386	if err != nil {
387		return []IPListItem{}, err
388	}
389
390	err = api.pollIPListBulkOperation(ctx, result.Result.OperationID)
391	if err != nil {
392		return []IPListItem{}, err
393	}
394
395	return api.ListIPListItems(ctx, id)
396}
397
398// GetIPListItem returns a single IP List Item
399//
400// API reference: https://api.cloudflare.com/#rules-lists-get-list-item
401func (api *API) GetIPListItem(ctx context.Context, listID, id string) (IPListItem, error) {
402	uri := fmt.Sprintf("/accounts/%s/rules/lists/%s/items/%s", api.AccountID, listID, id)
403	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
404	if err != nil {
405		return IPListItem{}, err
406	}
407
408	result := IPListItemsGetResponse{}
409	if err := json.Unmarshal(res, &result); err != nil {
410		return IPListItem{}, errors.Wrap(err, errUnmarshalError)
411	}
412
413	return result.Result, nil
414}
415
416// GetIPListBulkOperation returns the status of a bulk operation
417//
418// API reference: https://api.cloudflare.com/#rules-lists-get-bulk-operation
419func (api *API) GetIPListBulkOperation(ctx context.Context, id string) (IPListBulkOperation, error) {
420	uri := fmt.Sprintf("/accounts/%s/rules/lists/bulk_operations/%s", api.AccountID, id)
421	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
422	if err != nil {
423		return IPListBulkOperation{}, err
424	}
425
426	result := IPListBulkOperationResponse{}
427	if err := json.Unmarshal(res, &result); err != nil {
428		return IPListBulkOperation{}, errors.Wrap(err, errUnmarshalError)
429	}
430
431	return result.Result, nil
432}
433
434// pollIPListBulkOperation implements synchronous behaviour for some asynchronous endpoints.
435// bulk-operation status can be either pending, running, failed or completed
436func (api *API) pollIPListBulkOperation(ctx context.Context, id string) error {
437	for i := uint8(0); i < 16; i++ {
438		sleepDuration := 1 << (i / 2) * time.Second
439		select {
440		case <-time.After(sleepDuration):
441		case <-ctx.Done():
442			return errors.Wrap(ctx.Err(), "operation aborted during backoff")
443		}
444
445		bulkResult, err := api.GetIPListBulkOperation(ctx, id)
446		if err != nil {
447			return err
448		}
449
450		switch bulkResult.Status {
451		case "failed":
452			return errors.New(bulkResult.Error)
453		case "pending", "running":
454			continue
455		case "completed":
456			return nil
457		default:
458			return errors.New(fmt.Sprintf("%s: %s", errOperationUnexpectedStatus, bulkResult.Status))
459		}
460	}
461
462	return errors.New(errOperationStillRunning)
463}
464