1package cloudflare
2
3import (
4	"bytes"
5	"context"
6	"encoding/hex"
7	"encoding/json"
8	"fmt"
9	"io"
10	"math/rand"
11	"mime/multipart"
12	"net/http"
13	"net/textproto"
14	"time"
15
16	"github.com/pkg/errors"
17)
18
19// WorkerRequestParams provides parameters for worker requests for both enterprise and standard requests
20type WorkerRequestParams struct {
21	ZoneID     string
22	ScriptName string
23}
24
25// WorkerScriptParams provides a worker script and the associated bindings
26type WorkerScriptParams struct {
27	Script string
28
29	// Bindings should be a map where the keys are the binding name, and the
30	// values are the binding content
31	Bindings map[string]WorkerBinding
32}
33
34// WorkerRoute is used to map traffic matching a URL pattern to a workers
35//
36// API reference: https://api.cloudflare.com/#worker-routes-properties
37type WorkerRoute struct {
38	ID      string `json:"id,omitempty"`
39	Pattern string `json:"pattern"`
40	Enabled bool   `json:"enabled"` // this is deprecated: https://api.cloudflare.com/#worker-filters-deprecated--properties
41	Script  string `json:"script,omitempty"`
42}
43
44// WorkerRoutesResponse embeds Response struct and slice of WorkerRoutes
45type WorkerRoutesResponse struct {
46	Response
47	Routes []WorkerRoute `json:"result"`
48}
49
50// WorkerRouteResponse embeds Response struct and a single WorkerRoute
51type WorkerRouteResponse struct {
52	Response
53	WorkerRoute `json:"result"`
54}
55
56// WorkerScript Cloudflare Worker struct with metadata
57type WorkerScript struct {
58	WorkerMetaData
59	Script string `json:"script"`
60}
61
62// WorkerMetaData contains worker script information such as size, creation & modification dates
63type WorkerMetaData struct {
64	ID         string    `json:"id,omitempty"`
65	ETAG       string    `json:"etag,omitempty"`
66	Size       int       `json:"size,omitempty"`
67	CreatedOn  time.Time `json:"created_on,omitempty"`
68	ModifiedOn time.Time `json:"modified_on,omitempty"`
69}
70
71// WorkerListResponse wrapper struct for API response to worker script list API call
72type WorkerListResponse struct {
73	Response
74	WorkerList []WorkerMetaData `json:"result"`
75}
76
77// WorkerScriptResponse wrapper struct for API response to worker script calls
78type WorkerScriptResponse struct {
79	Response
80	WorkerScript `json:"result"`
81}
82
83// WorkerBindingType represents a particular type of binding
84type WorkerBindingType string
85
86func (b WorkerBindingType) String() string {
87	return string(b)
88}
89
90const (
91	// WorkerInheritBindingType is the type for inherited bindings
92	WorkerInheritBindingType WorkerBindingType = "inherit"
93	// WorkerKvNamespaceBindingType is the type for KV Namespace bindings
94	WorkerKvNamespaceBindingType WorkerBindingType = "kv_namespace"
95	// WorkerWebAssemblyBindingType is the type for Web Assembly module bindings
96	WorkerWebAssemblyBindingType WorkerBindingType = "wasm_module"
97	// WorkerSecretTextBindingType is the type for secret text bindings
98	WorkerSecretTextBindingType WorkerBindingType = "secret_text"
99	// WorkerPlainTextBindingType is the type for plain text bindings
100	WorkerPlainTextBindingType WorkerBindingType = "plain_text"
101)
102
103// WorkerBindingListItem a struct representing an individual binding in a list of bindings
104type WorkerBindingListItem struct {
105	Name    string `json:"name"`
106	Binding WorkerBinding
107}
108
109// WorkerBindingListResponse wrapper struct for API response to worker binding list API call
110type WorkerBindingListResponse struct {
111	Response
112	BindingList []WorkerBindingListItem
113}
114
115// Workers supports multiple types of bindings, e.g. KV namespaces or WebAssembly modules, and each type
116// of binding will be represented differently in the upload request body. At a high-level, every binding
117// will specify metadata, which is a JSON object with the properties "name" and "type". Some types of bindings
118// will also have additional metadata properties. For example, KV bindings also specify the KV namespace.
119// In addition to the metadata, some binding types may need to include additional data as part of the
120// multipart form. For example, WebAssembly bindings will include the contents of the WebAssembly module.
121
122// WorkerBinding is the generic interface implemented by all of
123// the various binding types
124type WorkerBinding interface {
125	Type() WorkerBindingType
126
127	// serialize is responsible for returning the binding metadata as well as an optionally
128	// returning a function that can modify the multipart form body. For example, this is used
129	// by WebAssembly bindings to add a new part containing the WebAssembly module contents.
130	serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error)
131}
132
133// workerBindingMeta is the metadata portion of the binding
134type workerBindingMeta = map[string]interface{}
135
136// workerBindingBodyWriter allows for a binding to add additional parts to the multipart body
137type workerBindingBodyWriter func(*multipart.Writer) error
138
139// WorkerInheritBinding will just persist whatever binding content was previously uploaded
140type WorkerInheritBinding struct {
141	// Optional parameter that allows for renaming a binding without changing
142	// its contents. If `OldName` is empty, the binding name will not be changed.
143	OldName string
144}
145
146// Type returns the type of the binding
147func (b WorkerInheritBinding) Type() WorkerBindingType {
148	return WorkerInheritBindingType
149}
150
151func (b WorkerInheritBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) {
152	meta := workerBindingMeta{
153		"name": bindingName,
154		"type": b.Type(),
155	}
156
157	if b.OldName != "" {
158		meta["old_name"] = b.OldName
159	}
160
161	return meta, nil, nil
162}
163
164// WorkerKvNamespaceBinding is a binding to a Workers KV Namespace
165//
166// https://developers.cloudflare.com/workers/archive/api/resource-bindings/kv-namespaces/
167type WorkerKvNamespaceBinding struct {
168	NamespaceID string
169}
170
171// Type returns the type of the binding
172func (b WorkerKvNamespaceBinding) Type() WorkerBindingType {
173	return WorkerKvNamespaceBindingType
174}
175
176func (b WorkerKvNamespaceBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) {
177	if b.NamespaceID == "" {
178		return nil, nil, errors.Errorf(`NamespaceID for binding "%s" cannot be empty`, bindingName)
179	}
180
181	return workerBindingMeta{
182		"name":         bindingName,
183		"type":         b.Type(),
184		"namespace_id": b.NamespaceID,
185	}, nil, nil
186}
187
188// WorkerWebAssemblyBinding is a binding to a WebAssembly module
189//
190// https://developers.cloudflare.com/workers/archive/api/resource-bindings/webassembly-modules/
191type WorkerWebAssemblyBinding struct {
192	Module io.Reader
193}
194
195// Type returns the type of the binding
196func (b WorkerWebAssemblyBinding) Type() WorkerBindingType {
197	return WorkerWebAssemblyBindingType
198}
199
200func (b WorkerWebAssemblyBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) {
201	partName := getRandomPartName()
202
203	bodyWriter := func(mpw *multipart.Writer) error {
204		var hdr = textproto.MIMEHeader{}
205		hdr.Set("content-disposition", fmt.Sprintf(`form-data; name="%s"`, partName))
206		hdr.Set("content-type", "application/wasm")
207		pw, err := mpw.CreatePart(hdr)
208		if err != nil {
209			return err
210		}
211		_, err = io.Copy(pw, b.Module)
212		return err
213	}
214
215	return workerBindingMeta{
216		"name": bindingName,
217		"type": b.Type(),
218		"part": partName,
219	}, bodyWriter, nil
220}
221
222// WorkerPlainTextBinding is a binding to plain text
223//
224// https://developers.cloudflare.com/workers/tooling/api/scripts/#add-a-plain-text-binding
225type WorkerPlainTextBinding struct {
226	Text string
227}
228
229// Type returns the type of the binding
230func (b WorkerPlainTextBinding) Type() WorkerBindingType {
231	return WorkerPlainTextBindingType
232}
233
234func (b WorkerPlainTextBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) {
235	if b.Text == "" {
236		return nil, nil, errors.Errorf(`Text for binding "%s" cannot be empty`, bindingName)
237	}
238
239	return workerBindingMeta{
240		"name": bindingName,
241		"type": b.Type(),
242		"text": b.Text,
243	}, nil, nil
244}
245
246// WorkerSecretTextBinding is a binding to secret text
247//
248// https://developers.cloudflare.com/workers/tooling/api/scripts/#add-a-secret-text-binding
249type WorkerSecretTextBinding struct {
250	Text string
251}
252
253// Type returns the type of the binding
254func (b WorkerSecretTextBinding) Type() WorkerBindingType {
255	return WorkerSecretTextBindingType
256}
257
258func (b WorkerSecretTextBinding) serialize(bindingName string) (workerBindingMeta, workerBindingBodyWriter, error) {
259	if b.Text == "" {
260		return nil, nil, errors.Errorf(`Text for binding "%s" cannot be empty`, bindingName)
261	}
262
263	return workerBindingMeta{
264		"name": bindingName,
265		"type": b.Type(),
266		"text": b.Text,
267	}, nil, nil
268}
269
270// Each binding that adds a part to the multipart form body will need
271// a unique part name so we just generate a random 128bit hex string
272func getRandomPartName() string {
273	randBytes := make([]byte, 16)
274	rand.Read(randBytes)
275	return hex.EncodeToString(randBytes)
276}
277
278// DeleteWorker deletes worker for a zone.
279//
280// API reference: https://api.cloudflare.com/#worker-script-delete-worker
281func (api *API) DeleteWorker(ctx context.Context, requestParams *WorkerRequestParams) (WorkerScriptResponse, error) {
282	// if ScriptName is provided we will treat as org request
283	if requestParams.ScriptName != "" {
284		return api.deleteWorkerWithName(ctx, requestParams.ScriptName)
285	}
286	uri := fmt.Sprintf("/zones/%s/workers/script", requestParams.ZoneID)
287	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
288	var r WorkerScriptResponse
289	if err != nil {
290		return r, err
291	}
292	err = json.Unmarshal(res, &r)
293	if err != nil {
294		return r, errors.Wrap(err, errUnmarshalError)
295	}
296	return r, nil
297}
298
299// DeleteWorkerWithName deletes worker for a zone.
300// Sccount must be specified as api option https://godoc.org/github.com/cloudflare/cloudflare-go#UsingAccount
301//
302// API reference: https://developers.cloudflare.com/workers/tooling/api/scripts/
303func (api *API) deleteWorkerWithName(ctx context.Context, scriptName string) (WorkerScriptResponse, error) {
304	if api.AccountID == "" {
305		return WorkerScriptResponse{}, errors.New("account ID required")
306	}
307	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", api.AccountID, scriptName)
308	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
309	var r WorkerScriptResponse
310	if err != nil {
311		return r, err
312	}
313	err = json.Unmarshal(res, &r)
314	if err != nil {
315		return r, errors.Wrap(err, errUnmarshalError)
316	}
317	return r, nil
318}
319
320// DownloadWorker fetch raw script content for your worker returns []byte containing worker code js
321//
322// API reference: https://api.cloudflare.com/#worker-script-download-worker
323func (api *API) DownloadWorker(ctx context.Context, requestParams *WorkerRequestParams) (WorkerScriptResponse, error) {
324	if requestParams.ScriptName != "" {
325		return api.downloadWorkerWithName(ctx, requestParams.ScriptName)
326	}
327	uri := fmt.Sprintf("/zones/%s/workers/script", requestParams.ZoneID)
328	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
329	var r WorkerScriptResponse
330	if err != nil {
331		return r, err
332	}
333	r.Script = string(res)
334	r.Success = true
335	return r, nil
336}
337
338// DownloadWorkerWithName fetch raw script content for your worker returns string containing worker code js
339//
340// API reference: https://developers.cloudflare.com/workers/tooling/api/scripts/
341func (api *API) downloadWorkerWithName(ctx context.Context, scriptName string) (WorkerScriptResponse, error) {
342	if api.AccountID == "" {
343		return WorkerScriptResponse{}, errors.New("account ID required")
344	}
345	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", api.AccountID, scriptName)
346	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
347	var r WorkerScriptResponse
348	if err != nil {
349		return r, err
350	}
351	r.Script = string(res)
352	r.Success = true
353	return r, nil
354}
355
356// ListWorkerBindings returns all the bindings for a particular worker
357func (api *API) ListWorkerBindings(ctx context.Context, requestParams *WorkerRequestParams) (WorkerBindingListResponse, error) {
358	if requestParams.ScriptName == "" {
359		return WorkerBindingListResponse{}, errors.New("ScriptName is required")
360	}
361	if api.AccountID == "" {
362		return WorkerBindingListResponse{}, errors.New("account ID required")
363	}
364
365	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/bindings", api.AccountID, requestParams.ScriptName)
366
367	var jsonRes struct {
368		Response
369		Bindings []workerBindingMeta `json:"result"`
370	}
371	var r WorkerBindingListResponse
372	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
373	if err != nil {
374		return r, err
375	}
376	err = json.Unmarshal(res, &jsonRes)
377	if err != nil {
378		return r, errors.Wrap(err, errUnmarshalError)
379	}
380
381	r = WorkerBindingListResponse{
382		Response:    jsonRes.Response,
383		BindingList: make([]WorkerBindingListItem, 0, len(jsonRes.Bindings)),
384	}
385	for _, jsonBinding := range jsonRes.Bindings {
386		name, ok := jsonBinding["name"].(string)
387		if !ok {
388			return r, errors.Errorf("Binding missing name %v", jsonBinding)
389		}
390		bType, ok := jsonBinding["type"].(string)
391		if !ok {
392			return r, errors.Errorf("Binding missing type %v", jsonBinding)
393		}
394		bindingListItem := WorkerBindingListItem{
395			Name: name,
396		}
397
398		switch WorkerBindingType(bType) {
399		case WorkerKvNamespaceBindingType:
400			namespaceID := jsonBinding["namespace_id"].(string)
401			bindingListItem.Binding = WorkerKvNamespaceBinding{
402				NamespaceID: namespaceID,
403			}
404		case WorkerWebAssemblyBindingType:
405			bindingListItem.Binding = WorkerWebAssemblyBinding{
406				Module: &bindingContentReader{
407					api:           api,
408					requestParams: requestParams,
409					bindingName:   name,
410				},
411			}
412		case WorkerPlainTextBindingType:
413			text := jsonBinding["text"].(string)
414			bindingListItem.Binding = WorkerPlainTextBinding{
415				Text: text,
416			}
417		case WorkerSecretTextBindingType:
418			bindingListItem.Binding = WorkerSecretTextBinding{}
419		default:
420			bindingListItem.Binding = WorkerInheritBinding{}
421		}
422		r.BindingList = append(r.BindingList, bindingListItem)
423	}
424
425	return r, nil
426}
427
428// bindingContentReader is an io.Reader that will lazily load the
429// raw bytes for a binding from the API when the Read() method
430// is first called. This is only useful for binding types
431// that store raw bytes, like WebAssembly modules
432type bindingContentReader struct {
433	api           *API
434	requestParams *WorkerRequestParams
435	bindingName   string
436	content       []byte
437	position      int
438}
439
440func (b *bindingContentReader) Read(p []byte) (n int, err error) {
441	// Lazily load the content when Read() is first called
442	if b.content == nil {
443		uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s/bindings/%s/content", b.api.AccountID, b.requestParams.ScriptName, b.bindingName)
444		res, err := b.api.makeRequest(http.MethodGet, uri, nil)
445		if err != nil {
446			return 0, err
447		}
448		b.content = res
449	}
450
451	if b.position >= len(b.content) {
452		return 0, io.EOF
453	}
454
455	bytesRemaining := len(b.content) - b.position
456	bytesToProcess := 0
457	if len(p) < bytesRemaining {
458		bytesToProcess = len(p)
459	} else {
460		bytesToProcess = bytesRemaining
461	}
462
463	for i := 0; i < bytesToProcess; i++ {
464		p[i] = b.content[b.position]
465		b.position = b.position + 1
466	}
467
468	return bytesToProcess, nil
469}
470
471// ListWorkerScripts returns list of worker scripts for given account.
472//
473// API reference: https://developers.cloudflare.com/workers/tooling/api/scripts/
474func (api *API) ListWorkerScripts(ctx context.Context) (WorkerListResponse, error) {
475	if api.AccountID == "" {
476		return WorkerListResponse{}, errors.New("account ID required")
477	}
478	uri := fmt.Sprintf("/accounts/%s/workers/scripts", api.AccountID)
479	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
480	if err != nil {
481		return WorkerListResponse{}, err
482	}
483	var r WorkerListResponse
484	err = json.Unmarshal(res, &r)
485	if err != nil {
486		return WorkerListResponse{}, errors.Wrap(err, errUnmarshalError)
487	}
488	return r, nil
489}
490
491// UploadWorker push raw script content for your worker.
492//
493// API reference: https://api.cloudflare.com/#worker-script-upload-worker
494func (api *API) UploadWorker(ctx context.Context, requestParams *WorkerRequestParams, data string) (WorkerScriptResponse, error) {
495	if requestParams.ScriptName != "" {
496		return api.uploadWorkerWithName(ctx, requestParams.ScriptName, "application/javascript", []byte(data))
497	}
498	return api.uploadWorkerForZone(ctx, requestParams.ZoneID, "application/javascript", []byte(data))
499}
500
501// UploadWorkerWithBindings push raw script content and bindings for your worker
502//
503// API reference: https://api.cloudflare.com/#worker-script-upload-worker
504func (api *API) UploadWorkerWithBindings(ctx context.Context, requestParams *WorkerRequestParams, data *WorkerScriptParams) (WorkerScriptResponse, error) {
505	contentType, body, err := formatMultipartBody(data)
506	if err != nil {
507		return WorkerScriptResponse{}, err
508	}
509	if requestParams.ScriptName != "" {
510		return api.uploadWorkerWithName(ctx, requestParams.ScriptName, contentType, body)
511	}
512	return api.uploadWorkerForZone(ctx, requestParams.ZoneID, contentType, body)
513}
514
515func (api *API) uploadWorkerForZone(ctx context.Context, zoneID, contentType string, body []byte) (WorkerScriptResponse, error) {
516	uri := fmt.Sprintf("/zones/%s/workers/script", zoneID)
517	headers := make(http.Header)
518	headers.Set("Content-Type", contentType)
519	res, err := api.makeRequestContextWithHeaders(ctx, http.MethodPut, uri, body, headers)
520	var r WorkerScriptResponse
521	if err != nil {
522		return r, err
523	}
524	err = json.Unmarshal(res, &r)
525	if err != nil {
526		return r, errors.Wrap(err, errUnmarshalError)
527	}
528	return r, nil
529}
530
531func (api *API) uploadWorkerWithName(ctx context.Context, scriptName, contentType string, body []byte) (WorkerScriptResponse, error) {
532	if api.AccountID == "" {
533		return WorkerScriptResponse{}, errors.New("account ID required")
534	}
535	uri := fmt.Sprintf("/accounts/%s/workers/scripts/%s", api.AccountID, scriptName)
536	headers := make(http.Header)
537	headers.Set("Content-Type", contentType)
538	res, err := api.makeRequestContextWithHeaders(ctx, http.MethodPut, uri, body, headers)
539	var r WorkerScriptResponse
540	if err != nil {
541		return r, err
542	}
543	err = json.Unmarshal(res, &r)
544	if err != nil {
545		return r, errors.Wrap(err, errUnmarshalError)
546	}
547	return r, nil
548}
549
550// Returns content-type, body, error
551func formatMultipartBody(params *WorkerScriptParams) (string, []byte, error) {
552	var buf = &bytes.Buffer{}
553	var mpw = multipart.NewWriter(buf)
554	defer mpw.Close()
555
556	// Write metadata part
557	scriptPartName := "script"
558	meta := struct {
559		BodyPart string              `json:"body_part"`
560		Bindings []workerBindingMeta `json:"bindings"`
561	}{
562		BodyPart: scriptPartName,
563		Bindings: make([]workerBindingMeta, 0, len(params.Bindings)),
564	}
565
566	bodyWriters := make([]workerBindingBodyWriter, 0, len(params.Bindings))
567	for name, b := range params.Bindings {
568		bindingMeta, bodyWriter, err := b.serialize(name)
569		if err != nil {
570			return "", nil, err
571		}
572
573		meta.Bindings = append(meta.Bindings, bindingMeta)
574		bodyWriters = append(bodyWriters, bodyWriter)
575	}
576
577	var hdr = textproto.MIMEHeader{}
578	hdr.Set("content-disposition", fmt.Sprintf(`form-data; name="%s"`, "metadata"))
579	hdr.Set("content-type", "application/json")
580	pw, err := mpw.CreatePart(hdr)
581	if err != nil {
582		return "", nil, err
583	}
584	metaJSON, err := json.Marshal(meta)
585	if err != nil {
586		return "", nil, err
587	}
588	_, err = pw.Write(metaJSON)
589	if err != nil {
590		return "", nil, err
591	}
592
593	// Write script part
594	hdr = textproto.MIMEHeader{}
595	hdr.Set("content-disposition", fmt.Sprintf(`form-data; name="%s"`, scriptPartName))
596	hdr.Set("content-type", "application/javascript")
597	pw, err = mpw.CreatePart(hdr)
598	if err != nil {
599		return "", nil, err
600	}
601	_, err = pw.Write([]byte(params.Script))
602	if err != nil {
603		return "", nil, err
604	}
605
606	// Write other bindings with parts
607	for _, w := range bodyWriters {
608		if w != nil {
609			err = w(mpw)
610			if err != nil {
611				return "", nil, err
612			}
613		}
614	}
615
616	mpw.Close()
617
618	return mpw.FormDataContentType(), buf.Bytes(), nil
619}
620
621// CreateWorkerRoute creates worker route for a zone
622//
623// API reference: https://api.cloudflare.com/#worker-filters-create-filter, https://api.cloudflare.com/#worker-routes-create-route
624func (api *API) CreateWorkerRoute(ctx context.Context, zoneID string, route WorkerRoute) (WorkerRouteResponse, error) {
625	pathComponent, err := getRouteEndpoint(api, route)
626	if err != nil {
627		return WorkerRouteResponse{}, err
628	}
629
630	uri := fmt.Sprintf("/zones/%s/workers/%s", zoneID, pathComponent)
631	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, route)
632	if err != nil {
633		return WorkerRouteResponse{}, err
634	}
635	var r WorkerRouteResponse
636	err = json.Unmarshal(res, &r)
637	if err != nil {
638		return WorkerRouteResponse{}, errors.Wrap(err, errUnmarshalError)
639	}
640	return r, nil
641}
642
643// DeleteWorkerRoute deletes worker route for a zone
644//
645// API reference: https://api.cloudflare.com/#worker-routes-delete-route
646func (api *API) DeleteWorkerRoute(ctx context.Context, zoneID string, routeID string) (WorkerRouteResponse, error) {
647	uri := fmt.Sprintf("/zones/%s/workers/routes/%s", zoneID, routeID)
648	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
649	if err != nil {
650		return WorkerRouteResponse{}, err
651	}
652	var r WorkerRouteResponse
653	err = json.Unmarshal(res, &r)
654	if err != nil {
655		return WorkerRouteResponse{}, errors.Wrap(err, errUnmarshalError)
656	}
657	return r, nil
658}
659
660// ListWorkerRoutes returns list of worker routes
661//
662// API reference: https://api.cloudflare.com/#worker-filters-list-filters, https://api.cloudflare.com/#worker-routes-list-routes
663func (api *API) ListWorkerRoutes(ctx context.Context, zoneID string) (WorkerRoutesResponse, error) {
664	pathComponent := "filters"
665	// Unfortunately we don't have a good signal of whether the user is wanting
666	// to use the deprecated filters endpoint (https://api.cloudflare.com/#worker-filters-list-filters)
667	// or the multi-script routes endpoint (https://api.cloudflare.com/#worker-script-list-workers)
668	//
669	// The filters endpoint does not support API tokens, so if an API token is specified we need to use
670	// the routes endpoint. Otherwise, since the multi-script API endpoints that operate on a script
671	// require an AccountID, we assume that anyone specifying an AccountID is using the routes endpoint.
672	// This is likely too presumptuous. In the next major version, we should just remove the deprecated
673	// filter endpoints entirely to avoid this ambiguity.
674	if api.AccountID != "" || api.APIToken != "" {
675		pathComponent = "routes"
676	}
677	uri := fmt.Sprintf("/zones/%s/workers/%s", zoneID, pathComponent)
678	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
679	if err != nil {
680		return WorkerRoutesResponse{}, err
681	}
682	var r WorkerRoutesResponse
683	err = json.Unmarshal(res, &r)
684	if err != nil {
685		return WorkerRoutesResponse{}, errors.Wrap(err, errUnmarshalError)
686	}
687	for i := range r.Routes {
688		route := &r.Routes[i]
689		// The Enabled flag will not be set in the multi-script API response
690		// so we manually set it to true if the script name is not empty
691		// in case any multi-script customers rely on the Enabled field
692		if route.Script != "" {
693			route.Enabled = true
694		}
695	}
696	return r, nil
697}
698
699// GetWorkerRoute returns a worker route.
700//
701// API reference: https://api.cloudflare.com/#worker-routes-get-route
702func (api *API) GetWorkerRoute(ctx context.Context, zoneID string, routeID string) (WorkerRouteResponse, error) {
703	uri := fmt.Sprintf("/zones/%s/workers/routes/%s", zoneID, routeID)
704	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
705	if err != nil {
706		return WorkerRouteResponse{}, err
707	}
708	var r WorkerRouteResponse
709	err = json.Unmarshal(res, &r)
710	if err != nil {
711		return WorkerRouteResponse{}, errors.Wrap(err, errUnmarshalError)
712	}
713	return r, nil
714}
715
716// UpdateWorkerRoute updates worker route for a zone.
717//
718// API reference: https://api.cloudflare.com/#worker-filters-update-filter, https://api.cloudflare.com/#worker-routes-update-route
719func (api *API) UpdateWorkerRoute(ctx context.Context, zoneID string, routeID string, route WorkerRoute) (WorkerRouteResponse, error) {
720	pathComponent, err := getRouteEndpoint(api, route)
721	if err != nil {
722		return WorkerRouteResponse{}, err
723	}
724	uri := fmt.Sprintf("/zones/%s/workers/%s/%s", zoneID, pathComponent, routeID)
725	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, route)
726	if err != nil {
727		return WorkerRouteResponse{}, err
728	}
729	var r WorkerRouteResponse
730	err = json.Unmarshal(res, &r)
731	if err != nil {
732		return WorkerRouteResponse{}, errors.Wrap(err, errUnmarshalError)
733	}
734	return r, nil
735}
736
737func getRouteEndpoint(api *API, route WorkerRoute) (string, error) {
738	if route.Script != "" && route.Enabled {
739		return "", errors.New("Only `Script` or `Enabled` may be specified for a WorkerRoute, not both")
740	}
741
742	// For backwards-compatibility, fallback to the deprecated filter
743	// endpoint if Enabled == true
744	// https://api.cloudflare.com/#worker-filters-deprecated--properties
745	if route.Enabled {
746		return "filters", nil
747	}
748
749	return "routes", nil
750}
751