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