From a35567cfce312ca0c6a51e0908f56a152ffbb9c7 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Tue, 12 Nov 2024 21:54:35 +0100 Subject: [PATCH] Update code from service worker to module worker format --- .../reference/subdomain-subdirectories.mdx | 33 +- .../control-origin-access.mdx | 123 +++---- .../refactor-a-worker-to-pages-functions.mdx | 169 +++++---- ...onfigure-your-mobile-app-or-iot-device.mdx | 18 +- .../viewing-videos/securing-your-stream.mdx | 325 ++++++++++-------- .../waiting-room/how-to/json-response.mdx | 89 +++-- .../workers/reference/how-the-cache-works.mdx | 2 +- 7 files changed, 383 insertions(+), 376 deletions(-) diff --git a/src/content/docs/automatic-platform-optimization/reference/subdomain-subdirectories.mdx b/src/content/docs/automatic-platform-optimization/reference/subdomain-subdirectories.mdx index 99a81df8e0b9d3..1291c9da822992 100644 --- a/src/content/docs/automatic-platform-optimization/reference/subdomain-subdirectories.mdx +++ b/src/content/docs/automatic-platform-optimization/reference/subdomain-subdirectories.mdx @@ -3,7 +3,6 @@ title: Subdomains and subdirectories pcx_content_type: how-to sidebar: order: 14 - --- ## Run APO on a subdomain @@ -36,24 +35,20 @@ If you choose to run APO only on a subdirectory, the rest of the domain should b The `cf-edge-cache: no-cache` instructs the APO service to bypass caching for non-WordPress parts of the site. You can implement this option with Cloudflare Workers using the example below. ```js -addEventListener('fetch', event => { - event.respondWith(handleRequest(event.request)); -}); - -async function handleRequest(request) { - /** - * Response properties are immutable. To change them, construct a new - * Response object. Response headers can be modified through the headers `set` method. - */ - const originalResponse = await fetch(request); - - let response = new Response(originalResponse.body, originalResponse); - - // Add a header using set method - response.headers.set('cf-edge-cache', 'no-cache'); - - return response; -} +export default { + async fetch(request, env, ctx) { + const originalResponse = await fetch(request); + + /** + * Response properties are immutable. To change them, construct a new + * Response object. Response headers can be modified through the headers `set` method. + */ + const response = new Response(originalResponse.body, originalResponse); + response.headers.set("cf-edge-cache", "no-cache"); + + return response; + }, +}; ``` ### Use Cache Rules diff --git a/src/content/docs/images/transform-images/control-origin-access.mdx b/src/content/docs/images/transform-images/control-origin-access.mdx index aa0887beb27dd6..da1e81c79afd84 100644 --- a/src/content/docs/images/transform-images/control-origin-access.mdx +++ b/src/content/docs/images/transform-images/control-origin-access.mdx @@ -3,7 +3,6 @@ pcx_content_type: reference title: Control origin access sidebar: order: 24 - --- You can serve resized images without giving access to the original image. Images can be hosted on another server outside of your zone, and the true source of the image can be entirely hidden. The origin server may require authentication to disclose the original image, without needing visitors to be aware of it. Access to the full-size image may be prevented by making it impossible to manipulate resizing parameters. @@ -11,15 +10,13 @@ You can serve resized images without giving access to the original image. Images All these behaviors are completely customizable, because they are handled by custom code of a script running [on the edge in a Cloudflare Worker](/images/transform-images/transform-via-workers/). ```js -addEventListener("fetch", event => { - event.respondWith(handleRequest(event.request)) -}) - -async function handleRequest(request) { - // Here you can compute arbitrary imageURL and - // resizingOptions from any request data ... - return fetch(imageURL, {cf:{image:resizingOptions}}) -} +export default { + async fetch(request, env, ctx) { + // Here you can compute arbitrary imageURL and + // resizingOptions from any request data ... + return fetch(imageURL, { cf: { image: resizingOptions } }); + }, +}; ``` This code will be run for every request, but the source code will not be accessible to website visitors. This allows the code to perform security checks and contain secrets required to access the images in a controlled manner. @@ -28,27 +25,27 @@ The examples below are only suggestions, and do not have to be followed exactly. :::caution[Warning] - When testing image transformations, make sure you deploy the script and test it from a regular web browser window. The preview in the dashboard does not simulate transformations. - ::: ## Hiding the image server ```js async function handleRequest(request) { - const resizingOptions = {/* resizing options will be demonstrated in the next example */} - - const hiddenImageOrigin = "https://secret.example.com/hidden-directory" - const requestURL = new URL(request.url) - // Append the request path such as "/assets/image1.jpg" to the hiddenImageOrigin. - // You could also process the path to add or remove directories, modify filenames, etc. - const imageURL = hiddenImageOrigin + requestURL.path - // This will fetch image from the given URL, but to the website's visitors this - // will appear as a response to the original request. Visitor’s browser will - // not see this URL. - return fetch(imageURL, {cf:{image:resizingOptions}}) + const resizingOptions = { + /* resizing options will be demonstrated in the next example */ + }; + + const hiddenImageOrigin = "https://secret.example.com/hidden-directory"; + const requestURL = new URL(request.url); + // Append the request path such as "/assets/image1.jpg" to the hiddenImageOrigin. + // You could also process the path to add or remove directories, modify filenames, etc. + const imageURL = hiddenImageOrigin + requestURL.path; + // This will fetch image from the given URL, but to the website's visitors this + // will appear as a response to the original request. Visitor’s browser will + // not see this URL. + return fetch(imageURL, { cf: { image: resizingOptions } }); } ``` @@ -79,28 +76,34 @@ You do not have to include actual pixel dimensions in the URL. You can embed siz ```js async function handleRequest(request) { - const requestURL = new URL(request.url) - const resizingOptions = {} - - // The regex selects the first path component after the "images" - // prefix, and the rest of the path (e.g. "/images/first/rest") - const match = requestURL.path.match(/images\/([^/]+)\/(.+)/) - - // You can require the first path component to be one of the - // predefined sizes only, and set actual dimensions accordingly. - switch (match && match[1]) { - case "small": resizingOptions.width = 300; break; - case "medium": resizingOptions.width = 600; break; - case "large": resizingOptions.width = 900; break; - default: - throw Error("invalid size"); - } - - // The remainder of the path may be used to locate the original - // image, e.g. here "/images/small/image1.jpg" would map to - // "https://storage.example.com/bucket/image1.jpg" resized to 300px. - const imageURL = "https://storage.example.com/bucket/" + match[2] - return fetch(imageURL, {cf:{image:resizingOptions}}) + const requestURL = new URL(request.url); + const resizingOptions = {}; + + // The regex selects the first path component after the "images" + // prefix, and the rest of the path (e.g. "/images/first/rest") + const match = requestURL.path.match(/images\/([^/]+)\/(.+)/); + + // You can require the first path component to be one of the + // predefined sizes only, and set actual dimensions accordingly. + switch (match && match[1]) { + case "small": + resizingOptions.width = 300; + break; + case "medium": + resizingOptions.width = 600; + break; + case "large": + resizingOptions.width = 900; + break; + default: + throw Error("invalid size"); + } + + // The remainder of the path may be used to locate the original + // image, e.g. here "/images/small/image1.jpg" would map to + // "https://storage.example.com/bucket/image1.jpg" resized to 300px. + const imageURL = "https://storage.example.com/bucket/" + match[2]; + return fetch(imageURL, { cf: { image: resizingOptions } }); } ``` @@ -111,7 +114,7 @@ Cloudflare image transformations cache resized images to aid performance. Images ```js null {9} // generate signed headers (application specific) const signedHeaders = generatedSignedHeaders(); - + fetch(private_url, { headers: signedHeaders cf: { @@ -125,20 +128,20 @@ fetch(private_url, { When using this code, the following headers are passed through to the origin, and allow your request to be successful: -* `Authorization` -* `Cookie` -* `x-amz-content-sha256` -* `x-amz-date` -* `x-ms-date` -* `x-ms-version` -* `x-sa-date` -* `cf-access-client-id` -* `cf-access-client-secret` +- `Authorization` +- `Cookie` +- `x-amz-content-sha256` +- `x-amz-date` +- `x-ms-date` +- `x-ms-version` +- `x-sa-date` +- `cf-access-client-id` +- `cf-access-client-secret` For more information, refer to: -* [AWS docs](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) -* [Azure docs](https://docs.microsoft.com/en-us/rest/api/storageservices/List-Containers2#request-headers) -* [Google Cloud docs](https://cloud.google.com/storage/docs/aws-simple-migration) -* [Cloudflare Zero Trust docs](/cloudflare-one/identity/service-tokens/) -* [SecureAuth docs](https://docs.secureauth.com/2104/en/authentication-api-guide.html) +- [AWS docs](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) +- [Azure docs](https://docs.microsoft.com/en-us/rest/api/storageservices/List-Containers2#request-headers) +- [Google Cloud docs](https://cloud.google.com/storage/docs/aws-simple-migration) +- [Cloudflare Zero Trust docs](/cloudflare-one/identity/service-tokens/) +- [SecureAuth docs](https://docs.secureauth.com/2104/en/authentication-api-guide.html) diff --git a/src/content/docs/pages/how-to/refactor-a-worker-to-pages-functions.mdx b/src/content/docs/pages/how-to/refactor-a-worker-to-pages-functions.mdx index 846f7f047e30d2..e1fd8fc0491648 100644 --- a/src/content/docs/pages/how-to/refactor-a-worker-to-pages-functions.mdx +++ b/src/content/docs/pages/how-to/refactor-a-worker-to-pages-functions.mdx @@ -1,7 +1,6 @@ --- pcx_content_type: how-to title: Refactor a Worker to a Pages Function - --- In this guide, you will learn how to refactor a Worker made to intake form submissions to a Pages Function that can be hosted on your Cloudflare Pages application. [Pages Functions](/pages/functions/) is a serverless function that lives within the same project directory as your application and is deployed with Cloudflare Pages. It enables you to run server-side code that adds dynamic functionality without running a dedicated server. You may want to refactor a Worker to a Pages Function for one of these reasons: @@ -11,17 +10,15 @@ In this guide, you will learn how to refactor a Worker made to intake form submi :::note - You can import your Worker to a Pages project without using Functions by creating a `_worker.js` file in the output directory of your Pages project. This [Advanced mode](/pages/functions/advanced-mode/) requires writing your Worker with [Module syntax](/workers/reference/migrate-to-module-workers/). However, when using the `_worker.js` file in Pages, the entire `/functions` directory is ignored – including its routing and middleware characteristics. - ::: ## General refactoring steps -1. Remove the `addEventListener()` method and its event response and replace it with the appropriate `OnRequest` method. Refer to [Functions](/pages/functions/get-started/) to select the appropriate method for your Function. +1. Remove the fetch handler and replace it with the appropriate `OnRequest` method. Refer to [Functions](/pages/functions/get-started/) to select the appropriate method for your Function. 2. Pass the `context` object as an argument to your new `OnRequest` method to access the properties of the context parameter: `request`,`env`,`params` and `next`. 3. Use middleware to handle logic that must be executed before or after route handlers. Learn more about [using Middleware](/pages/functions/middleware/) in the Functions documentation. @@ -47,63 +44,60 @@ This step creates the boilerplate to write your Airtable submission Worker. Afte The following code block shows an example of a Worker that handles Airtable form submission. -Every Worker will have the default response to a `fetch` action with a `request` handler. The `submitHandler` async function is called if the pathname of the work is `/submit`. This function checks that the request method is a `POST` request and then proceeds to parse and post the form entries to Airtable using your credentials, which you can store using [Wrangler `secret`](/workers/wrangler/commands/#secret). +The `submitHandler` async function is called if the pathname of the work is `/submit`. This function checks that the request method is a `POST` request and then proceeds to parse and post the form entries to Airtable using your credentials, which you can store using [Wrangler `secret`](/workers/wrangler/commands/#secret). ```js -addEventListener("fetch", (event) => { - event.respondWith(handleRequest(event.request)); -}); - +export default { + async fetch(request, env, ctx) { + const url = new URL(request.url); -async function handleRequest(request) { - const url = new URL(request.url); + if (url.pathname === "/submit") { + return submitHandler(request, env); + } - if (url.pathname === "/submit") { - return submitHandler(request); - } - - return fetch(request.url); -} + return fetch(request.url); + }, +}; -async function submitHandler(request) { - if (request.method !== "POST") { - return new Response("Method not allowed", { - status: 405, - }); - } - const body = await request.formData(); - - const { first_name, last_name, email, phone, subject, message } = - Object.fromEntries(body); - - const reqBody = { - fields: { - "First Name": first_name, - "Last Name": last_name, - Email: email, - "Phone number": phone, - Subject: subject, - Message: message, - }, - }; - - return HandleAirtableData(reqBody); +async function submitHandler(request, env) { + if (request.method !== "POST") { + return new Response("Method not allowed", { + status: 405, + }); + } + const body = await request.formData(); + + const { first_name, last_name, email, phone, subject, message } = + Object.fromEntries(body); + + const reqBody = { + fields: { + "First Name": first_name, + "Last Name": last_name, + Email: email, + "Phone number": phone, + Subject: subject, + Message: message, + }, + }; + + return HandleAirtableData(reqBody, env); } -const HandleAirtableData = (body) => { - return fetch( - `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/${encodeURIComponent( - AIRTABLE_TABLE_NAME - )}`, - { - method: "POST", - body: JSON.stringify(body), - headers: { - Authorization: `Bearer ${AIRTABLE_API_KEY}`, - "Content-type": `application/json`, - }, - } - ); +const HandleAirtableData = (body, env) => { + return fetch( + `https://api.airtable.com/v0/${env.AIRTABLE_BASE_ID}/${encodeURIComponent( + env.AIRTABLE_TABLE_NAME, + )}`, + { + method: "POST", + body: JSON.stringify(body), + headers: { + Authorization: `Bearer ${env.AIRTABLE_API_KEY}`, + "Content-type": `application/json`, + }, + }, + ); }; ``` @@ -117,7 +111,6 @@ Then, in the `form.js` file, export a single `onRequestPost`: export async function onRequestPost(context) { return await submitHandler(context); } - ``` Every Worker has an `addEventListener` to listen for `fetch` events, but you will not need this in a Pages Function. Instead, you will `export` a single `onRequest` function, and depending on the HTTPS request it handles, you will name it accordingly. Refer to [Function documentation](/pages/functions/get-started/) to select the appropriate method for your function. @@ -132,23 +125,23 @@ export async function onRequestPost(context) { } async function submitHandler(context) { - const body = await context.request.formData(); - - const { first_name, last_name, email, phone, subject, message } = - Object.fromEntries(body); - - const reqBody = { - fields: { - "First Name": first_name, - "Last Name": last_name, - Email: email, - "Phone number": phone, - Subject: subject, - Message: message, - }, - }; - - return HandleAirtableData({ body: reqBody, env: env }); + const body = await context.request.formData(); + + const { first_name, last_name, email, phone, subject, message } = + Object.fromEntries(body); + + const reqBody = { + fields: { + "First Name": first_name, + "Last Name": last_name, + Email: email, + "Phone number": phone, + Subject: subject, + Message: message, + }, + }; + + return HandleAirtableData({ body: reqBody, env: env }); } ``` @@ -157,19 +150,19 @@ Finally, create a `HandleAirtableData` function. This function will send a `fetc ```js // .. const HandleAirtableData = async function onRequest({ body, env }) { - return fetch( - `https://api.airtable.com/v0/${env.AIRTABLE_BASE_ID}/${encodeURIComponent( - env.AIRTABLE_TABLE_NAME - )}`, - { - method: "POST", - body: JSON.stringify(body), - headers: { - Authorization: `Bearer ${env.AIRTABLE_API_KEY}`, - "Content-type": `application/json`, - }, - } - ); + return fetch( + `https://api.airtable.com/v0/${env.AIRTABLE_BASE_ID}/${encodeURIComponent( + env.AIRTABLE_TABLE_NAME, + )}`, + { + method: "POST", + body: JSON.stringify(body), + headers: { + Authorization: `Bearer ${env.AIRTABLE_API_KEY}`, + "Content-type": `application/json`, + }, + }, + ); }; ``` @@ -177,6 +170,6 @@ You can test your Function [locally using Wrangler](/pages/functions/local-devel ## Related resources -* [HTML forms](/pages/tutorials/forms/) -* [Plugins documentation](/pages/functions/plugins/) -* [Functions documentation](/pages/functions/) +- [HTML forms](/pages/tutorials/forms/) +- [Plugins documentation](/pages/functions/plugins/) +- [Functions documentation](/pages/functions/) diff --git a/src/content/docs/ssl/client-certificates/configure-your-mobile-app-or-iot-device.mdx b/src/content/docs/ssl/client-certificates/configure-your-mobile-app-or-iot-device.mdx index 473b0fbbeb04f0..7acf04816204eb 100644 --- a/src/content/docs/ssl/client-certificates/configure-your-mobile-app-or-iot-device.mdx +++ b/src/content/docs/ssl/client-certificates/configure-your-mobile-app-or-iot-device.mdx @@ -83,17 +83,13 @@ async function getTemperatures(request) { } } -async function handleRequest(request) { - if (request.method === "POST") { - return addTemperature(request); - } else { - return getTemperatures(request); - } -} - -addEventListener("fetch", (event) => { - event.respondWith(handleRequest(event.request)); -}); +export default { + async fetch(request, env, ctx) { + return request.method === "POST" + ? addTemperature(request) + : getTemperatures(request); + }, +}; ``` --- diff --git a/src/content/docs/stream/viewing-videos/securing-your-stream.mdx b/src/content/docs/stream/viewing-videos/securing-your-stream.mdx index 01c433048f17c2..b17203ce3fb81b 100644 --- a/src/content/docs/stream/viewing-videos/securing-your-stream.mdx +++ b/src/content/docs/stream/viewing-videos/securing-your-stream.mdx @@ -3,18 +3,17 @@ pcx_content_type: how-to title: Secure your Stream sidebar: order: 4 - --- ## Signed URLs / Tokens -By default, videos on Stream can be viewed by anyone with just a video id. If you want to make your video private by default and only give access to certain users, you can use the signed URL feature. When you mark a video to require signed URL, it can no longer be accessed publicly with only the video id. Instead, the user will need a signed url token to watch or download the video. +By default, videos on Stream can be viewed by anyone with just a video id. If you want to make your video private by default and only give access to certain users, you can use the signed URL feature. When you mark a video to require signed URL, it can no longer be accessed publicly with only the video id. Instead, the user will need a signed url token to watch or download the video. Here are some common use cases for using signed URLs: -* Restricting access so only logged in members can watch a particular video -* Let users watch your video for a limited time period (ie. 24 hours) -* Restricting access based on geolocation +- Restricting access so only logged in members can watch a particular video +- Let users watch your video for a limited time period (ie. 24 hours) +- Restricting access based on geolocation ### Making a video require signed URLs @@ -48,9 +47,9 @@ Response: You can program your app to generate token in two ways: -* **Using the /token endpoint to generate signed tokens:** The simplest way to create a signed url token is by calling the /token endpoint. This is recommended for testing purposes or if you are generating less than 10,000 tokens per day. +- **Using the /token endpoint to generate signed tokens:** The simplest way to create a signed url token is by calling the /token endpoint. This is recommended for testing purposes or if you are generating less than 10,000 tokens per day. -* **Using an open-source library:** If you have tens of thousands of daily users and need to generate a high-volume of tokens without calling the /token endpoint *each time*, you can create tokens yourself. This way, you do not need to call a Stream endpoint each time you need to generate a token. +- **Using an open-source library:** If you have tens of thousands of daily users and need to generate a high-volume of tokens without calling the /token endpoint _each time_, you can create tokens yourself. This way, you do not need to call a Stream endpoint each time you need to generate a token. ## Option 1: Using the /token endpoint @@ -66,19 +65,26 @@ You will see a response similar to this if the request succeeds: ```json { - "result": { - "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNkYzkzNTk4MmY4MDc1ZjJlZjk2MTA2ZDg1ZmNkODM4In0.eyJraWQiOiJjZGM5MzU5ODJmODA3NWYyZWY5NjEwNmQ4NWZjZDgzOCIsImV4cCI6IjE2MjE4ODk2NTciLCJuYmYiOiIxNjIxODgyNDU3In0.iHGMvwOh2-SuqUG7kp2GeLXyKvMavP-I2rYCni9odNwms7imW429bM2tKs3G9INms8gSc7fzm8hNEYWOhGHWRBaaCs3U9H4DRWaFOvn0sJWLBitGuF_YaZM5O6fqJPTAwhgFKdikyk9zVzHrIJ0PfBL0NsTgwDxLkJjEAEULQJpiQU1DNm0w5ctasdbw77YtDwdZ01g924Dm6jIsWolW0Ic0AevCLyVdg501Ki9hSF7kYST0egcll47jmoMMni7ujQCJI1XEAOas32DdjnMvU8vXrYbaHk1m1oXlm319rDYghOHed9kr293KM7ivtZNlhYceSzOpyAmqNFS7mearyQ" - }, - "success": true, - "errors": [], - "messages": [] + "result": { + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImNkYzkzNTk4MmY4MDc1ZjJlZjk2MTA2ZDg1ZmNkODM4In0.eyJraWQiOiJjZGM5MzU5ODJmODA3NWYyZWY5NjEwNmQ4NWZjZDgzOCIsImV4cCI6IjE2MjE4ODk2NTciLCJuYmYiOiIxNjIxODgyNDU3In0.iHGMvwOh2-SuqUG7kp2GeLXyKvMavP-I2rYCni9odNwms7imW429bM2tKs3G9INms8gSc7fzm8hNEYWOhGHWRBaaCs3U9H4DRWaFOvn0sJWLBitGuF_YaZM5O6fqJPTAwhgFKdikyk9zVzHrIJ0PfBL0NsTgwDxLkJjEAEULQJpiQU1DNm0w5ctasdbw77YtDwdZ01g924Dm6jIsWolW0Ic0AevCLyVdg501Ki9hSF7kYST0egcll47jmoMMni7ujQCJI1XEAOas32DdjnMvU8vXrYbaHk1m1oXlm319rDYghOHed9kr293KM7ivtZNlhYceSzOpyAmqNFS7mearyQ" + }, + "success": true, + "errors": [], + "messages": [] } ``` To render the video, insert the `token` value in place of the `video id`: ```html - + ``` If you are using your own player, replace the video id in the manifest URL with the `token` value: @@ -90,71 +96,83 @@ If you are using your own player, replace the video id in the manifest URL with If you call the `/token` endpoint without any body, it will return a token that expires in one hour. Let's say you want to let a user watch a particular video for the next 12 hours. Here's how you'd do it with a Cloudflare Worker: ```javascript -addEventListener('fetch', event => { - event.respondWith(handleRequest(event)) -}) - -async function handleRequest(request) { - - var signed_url_restrictions = { - //limit viewing for the next 12 hours - exp: Math.floor(Date.now() / 1000) + (12*60*60) - }; - - const init = { - method: 'POST', - headers: { - "Authorization": "Bearer ", - "content-type": "application/json;charset=UTF-8" - }, - body: JSON.stringify(signed_url_restrictions) - } - const signedurl_service_response = await fetch("https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/{video_uid}/token", init) - return new Response(JSON.stringify(await signedurl_service_response.json()), {status: 200}) -} +export default { + async fetch(request, env, ctx) { + const signed_url_restrictions = { + //limit viewing for the next 12 hours + exp: Math.floor(Date.now() / 1000) + 12 * 60 * 60, + }; + + const init = { + method: "POST", + headers: { + Authorization: "Bearer ", + "content-type": "application/json;charset=UTF-8", + }, + body: JSON.stringify(signed_url_restrictions), + }; + + const signedurl_service_response = await fetch( + "https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/{video_uid}/token", + init, + ); + + return new Response( + JSON.stringify(await signedurl_service_response.json()), + { status: 200 }, + ); + }, +}; ``` The returned token will expire after 12 hours. Let's take this a step further and add 2 additional restrictions: -* Allow the signed URL token to be used for MP4 downloads (assuming the video has downloads enabled) -* Block users from US and Mexico from viewing or downloading the video +- Allow the signed URL token to be used for MP4 downloads (assuming the video has downloads enabled) +- Block users from US and Mexico from viewing or downloading the video To achieve this, we can specify additional restrictions in the `signed_url_restrictions` object in our sample code: ```javascript -addEventListener('fetch', event => { - event.respondWith(handleRequest(event)) -}) - -async function handleRequest(request) { - - var signed_url_restrictions = { - //limit viewing for the next 2 hours - exp: Math.floor(Date.now() / 1000) + (12*60*60), - downloadable: true, - accessRules:[{"type":"ip.geoip.country","country":["US","MX"],"action":"block"}] - }; - - const init = { - method: 'POST', - headers: { - "Authorization": "Bearer ", - "content-type": "application/json;charset=UTF-8" - }, - body: JSON.stringify(signed_url_restrictions) - } - const signedurl_service_response = await fetch("https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/{video_uid}/token", init) - return new Response(JSON.stringify(await signedurl_service_response.json()), {status: 200}) -} +export default { + async fetch(request, env, ctx) { + const signed_url_restrictions = { + //limit viewing for the next 2 hours + exp: Math.floor(Date.now() / 1000) + 12 * 60 * 60, + downloadable: true, + accessRules: [ + { type: "ip.geoip.country", country: ["US", "MX"], action: "block" }, + ], + }; + + const init = { + method: "POST", + headers: { + Authorization: "Bearer ", + "content-type": "application/json;charset=UTF-8", + }, + body: JSON.stringify(signed_url_restrictions), + }; + + const signedurl_service_response = await fetch( + "https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/{video_uid}/token", + init, + ); + + return new Response( + JSON.stringify(await signedurl_service_response.json()), + { status: 200 }, + ); + }, +}; ``` ## Option 2: Generating signed tokens without calling the `/token` endpoint If you are generating a high-volume of tokens, it is best to generate new tokens without needing to call the Stream API each time. -### Step 1: Call the `/stream/key` endpoint *once* to obtain a key +### Step 1: Call the `/stream/key` endpoint _once_ to obtain a key ```bash curl --request POST \ @@ -166,15 +184,15 @@ The response will return `pem` and `jwk` values. ```json { - "result": { - "id": "8f926b2b01f383510025a78a4dcbf6a", - "pem": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBemtHbXhCekFGMnBIMURiWmgyVGoyS3ZudlBVTkZmUWtNeXNCbzJlZzVqemRKTmRhCmtwMEphUHhoNkZxOTYveTBVd0lBNjdYeFdHb3kxcW1CRGhpdTVqekdtYW13NVgrYkR3TEdTVldGMEx3QnloMDYKN01Rb0xySHA3MDEycXBVNCtLODUyT1hMRVVlWVBrOHYzRlpTQ2VnMVdLRW5URC9oSmhVUTFsTmNKTWN3MXZUbQpHa2o0empBUTRBSFAvdHFERHFaZ3lMc1Vma2NsRDY3SVRkZktVZGtFU3lvVDVTcnFibHNFelBYcm9qaFlLWGk3CjFjak1yVDlFS0JCenhZSVEyOVRaZitnZU5ya0t4a2xMZTJzTUFML0VWZkFjdGkrc2ZqMkkyeEZKZmQ4aklmL2UKdHBCSVJZVDEza2FLdHUyYmk0R2IrV1BLK0toQjdTNnFGODlmTHdJREFRQUJBb0lCQUYzeXFuNytwNEtpM3ZmcgpTZmN4ZmRVV0xGYTEraEZyWk1mSHlaWEFJSnB1MDc0eHQ2ZzdqbXM3Tm0rTFVhSDV0N3R0bUxURTZacy91RXR0CjV3SmdQTjVUaFpTOXBmMUxPL3BBNWNmR2hFN1pMQ2wvV2ZVNXZpSFMyVDh1dGlRcUYwcXpLZkxCYk5kQW1MaWQKQWl4blJ6UUxDSzJIcmlvOW1KVHJtSUUvZENPdG80RUhYdHpZWjByOVordHRxMkZrd3pzZUdaK0tvd09JaWtvTgp2NWFOMVpmRGhEVG0wdG1Vd0tLbjBWcmZqalhRdFdjbFYxTWdRejhwM2xScWhISmJSK29PL1NMSXZqUE16dGxOCm5GV1ZEdTRmRHZsSjMyazJzSllNL2tRVUltT3V5alY3RTBBcm5vR2lBREdGZXFxK1UwajluNUFpNTJ6aTBmNloKdFdvwdju39xOFJWQkwxL2tvWFVmYk00S04ydVFadUdjaUdGNjlCRDJ1S3o1eGdvTwowVTBZNmlFNG9Cek5GUW5hWS9kayt5U1dsQWp2MkgraFBrTGpvZlRGSGlNTmUycUVNaUFaeTZ5cmRkSDY4VjdIClRNRllUQlZQaHIxT0dxZlRmc00vRktmZVhWY1FvMTI1RjBJQm5iWjNSYzRua1pNS0hzczUyWE1DZ1lFQTFQRVkKbGIybDU4blVianRZOFl6Uk1vQVo5aHJXMlhwM3JaZjE0Q0VUQ1dsVXFZdCtRN0NyN3dMQUVjbjdrbFk1RGF3QgpuTXJsZXl3S0crTUEvU0hlN3dQQkpNeDlVUGV4Q3YyRW8xT1loMTk3SGQzSk9zUythWWljemJsYmJqU0RqWXVjCkdSNzIrb1FlMzJjTXhjczJNRlBWcHVibjhjalBQbnZKd0k5aUpGVUNnWUVBMjM3UmNKSEdCTjVFM2FXLzd3ekcKbVBuUm1JSUczeW9UU0U3OFBtbHo2bXE5eTVvcSs5aFpaNE1Fdy9RbWFPMDF5U0xRdEY4QmY2TFN2RFh4QWtkdwpWMm5ra0svWWNhWDd3RHo0eWxwS0cxWTg3TzIwWWtkUXlxdjMybG1lN1JuVDhwcVBDQTRUWDloOWFVaXh6THNoCkplcGkvZFhRWFBWeFoxYXV4YldGL3VzQ2dZRUFxWnhVVWNsYVlYS2dzeUN3YXM0WVAxcEwwM3h6VDR5OTBOYXUKY05USFhnSzQvY2J2VHFsbGVaNCtNSzBxcGRmcDM5cjIrZFdlemVvNUx4YzBUV3Z5TDMxVkZhT1AyYk5CSUpqbwpVbE9ldFkwMitvWVM1NjJZWVdVQVNOandXNnFXY21NV2RlZjFIM3VuUDVqTVVxdlhRTTAxNjVnV2ZiN09YRjJyClNLYXNySFVDZ1lCYmRvL1orN1M3dEZSaDZlamJib2h3WGNDRVd4eXhXT2ZMcHdXNXdXT3dlWWZwWTh4cm5pNzQKdGRObHRoRXM4SHhTaTJudEh3TklLSEVlYmJ4eUh1UG5pQjhaWHBwNEJRNTYxczhjR1Z1ZSszbmVFUzBOTDcxZApQL1ZxUWpySFJrd3V5ckRFV2VCeEhUL0FvVEtEeSt3OTQ2SFM5V1dPTGJvbXQrd3g0NytNdWc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=", - "jwk": "eyJ1c2UiOiJzaWciLCJrdHkiOiJSU0EiLCJraWQiOiI4ZjkyNmIyYjAxZjM4MzUxNzAwMjVhNzhhNGRjYmY2YSIsImFsZyI6IlJTMjU2IiwibiI6InprR214QnpBRjJwSDFEYlpoMlRqMkt2bnZQVU5GZlFrTXlzQm8yZWc1anpkSk5kYWtwMEphUHhoNkZxOTZfeTBVd0lBNjdYeFdHb3kxcW1CRGhpdTVqekdtYW13NVgtYkR3TEdTVldGMEx3QnloMDY3TVFvTHJIcDcwMTJxcFU0LUs4NTJPWExFVWVZUGs4djNGWlNDZWcxV0tFblREX2hKaFVRMWxOY0pNY3cxdlRtR2tqNHpqQVE0QUhQX3RxRERxWmd5THNVZmtjbEQ2N0lUZGZLVWRrRVN5b1Q1U3JxYmxzRXpQWHJvamhZS1hpNzFjak1yVDlFS0JCenhZSVEyOVRaZi1nZU5ya0t4a2xMZTJzTUFMX0VWZkFjdGktc2ZqMkkyeEZKZmQ4aklmX2V0cEJJUllUMTNrYUt0dTJiaTRHYi1XUEstS2hCN1M2cUY4OWZMdyIsImUiOiJBUUFCIiwiZCI6IlhmS3FmdjZuZ3FMZTktdEo5ekY5MVJZc1ZyWDZFV3RreDhmSmxjQWdtbTdUdmpHM3FEdU9henMyYjR0Um9mbTN1MjJZdE1UcG16LTRTMjNuQW1BODNsT0ZsTDJsX1VzNy1rRGx4OGFFVHRrc0tYOVo5VG0tSWRMWlB5NjJKQ29YU3JNcDhzRnMxMENZdUowQ0xHZEhOQXNJcllldUtqMllsT3VZZ1Q5MEk2MmpnUWRlM05oblN2MW42MjJyWVdURE94NFpuNHFqQTRpS1NnMl9sbzNWbDhPRU5PYlMyWlRBb3FmUld0LU9OZEMxWnlWWFV5QkRQeW5lVkdxRWNsdEg2Zzc5SXNpLU04ek8yVTJjVlpVTzdoOE8tVW5mYVRhd2xnei1SQlFpWTY3S05Yc1RRQ3VlZ2FJQU1ZVjZxcjVUU1Ai2odx5iT0xSX3BtMWFpdktyUSIsInAiOiI5X1o5ZUpGTWI5X3E4UlZCTDFfa29YVWZiTTRLTjJ1UVp1R2NpR0Y2OUJEMnVLejV4Z29PMFUwWTZpRTRvQnpORlFuYVlfZGsteVNXbEFqdjJILWhQa0xqb2ZURkhpTU5lMnFFTWlBWnk2eXJkZEg2OFY3SFRNRllUQlZQaHIxT0dxZlRmc01fRktmZVhWY1FvMTI1RjBJQm5iWjNSYzRua1pNS0hzczUyWE0iLCJxIjoiMVBFWWxiMmw1OG5VYmp0WThZelJNb0FaOWhyVzJYcDNyWmYxNENFVENXbFVxWXQtUTdDcjd3TEFFY243a2xZNURhd0JuTXJsZXl3S0ctTUFfU0hlN3dQQkpNeDlVUGV4Q3YyRW8xT1loMTk3SGQzSk9zUy1hWWljemJsYmJqU0RqWXVjR1I3Mi1vUWUzMmNNeGNzMk1GUFZwdWJuOGNqUFBudkp3STlpSkZVIiwiZHAiOiIyMzdSY0pIR0JONUUzYVdfN3d6R21QblJtSUlHM3lvVFNFNzhQbWx6Nm1xOXk1b3EtOWhaWjRNRXdfUW1hTzAxeVNMUXRGOEJmNkxTdkRYeEFrZHdWMm5ra0tfWWNhWDd3RHo0eWxwS0cxWTg3TzIwWWtkUXlxdjMybG1lN1JuVDhwcVBDQTRUWDloOWFVaXh6THNoSmVwaV9kWFFYUFZ4WjFhdXhiV0ZfdXMiLCJkcSI6InFaeFVVY2xhWVhLZ3N5Q3dhczRZUDFwTDAzeHpUNHk5ME5hdWNOVEhYZ0s0X2NidlRxbGxlWjQtTUswcXBkZnAzOXIyLWRXZXplbzVMeGMwVFd2eUwzMVZGYU9QMmJOQklKam9VbE9ldFkwMi1vWVM1NjJZWVdVQVNOandXNnFXY21NV2RlZjFIM3VuUDVqTVVxdlhRTTAxNjVnV2ZiN09YRjJyU0thc3JIVSIsInFpIjoiVzNhUDJmdTB1N1JVWWVubzIyNkljRjNBaEZzY3NWam55NmNGdWNGanNIbUg2V1BNYTU0dS1MWFRaYllSTFBCOFVvdHA3UjhEU0NoeEhtMjhjaDdqNTRnZkdWNmFlQVVPZXRiUEhCbGJudnQ1M2hFdERTLTlYVF8xYWtJNngwWk1Mc3F3eEZuZ2NSMF93S0V5Zzh2c1BlT2gwdlZsamkyNkpyZnNNZU9fakxvIn0=", - "created": "2021-06-15T21:06:54.763937286Z" - }, - "success": true, - "errors": [], - "messages": [] + "result": { + "id": "8f926b2b01f383510025a78a4dcbf6a", + "pem": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBemtHbXhCekFGMnBIMURiWmgyVGoyS3ZudlBVTkZmUWtNeXNCbzJlZzVqemRKTmRhCmtwMEphUHhoNkZxOTYveTBVd0lBNjdYeFdHb3kxcW1CRGhpdTVqekdtYW13NVgrYkR3TEdTVldGMEx3QnloMDYKN01Rb0xySHA3MDEycXBVNCtLODUyT1hMRVVlWVBrOHYzRlpTQ2VnMVdLRW5URC9oSmhVUTFsTmNKTWN3MXZUbQpHa2o0empBUTRBSFAvdHFERHFaZ3lMc1Vma2NsRDY3SVRkZktVZGtFU3lvVDVTcnFibHNFelBYcm9qaFlLWGk3CjFjak1yVDlFS0JCenhZSVEyOVRaZitnZU5ya0t4a2xMZTJzTUFML0VWZkFjdGkrc2ZqMkkyeEZKZmQ4aklmL2UKdHBCSVJZVDEza2FLdHUyYmk0R2IrV1BLK0toQjdTNnFGODlmTHdJREFRQUJBb0lCQUYzeXFuNytwNEtpM3ZmcgpTZmN4ZmRVV0xGYTEraEZyWk1mSHlaWEFJSnB1MDc0eHQ2ZzdqbXM3Tm0rTFVhSDV0N3R0bUxURTZacy91RXR0CjV3SmdQTjVUaFpTOXBmMUxPL3BBNWNmR2hFN1pMQ2wvV2ZVNXZpSFMyVDh1dGlRcUYwcXpLZkxCYk5kQW1MaWQKQWl4blJ6UUxDSzJIcmlvOW1KVHJtSUUvZENPdG80RUhYdHpZWjByOVordHRxMkZrd3pzZUdaK0tvd09JaWtvTgp2NWFOMVpmRGhEVG0wdG1Vd0tLbjBWcmZqalhRdFdjbFYxTWdRejhwM2xScWhISmJSK29PL1NMSXZqUE16dGxOCm5GV1ZEdTRmRHZsSjMyazJzSllNL2tRVUltT3V5alY3RTBBcm5vR2lBREdGZXFxK1UwajluNUFpNTJ6aTBmNloKdFdvwdju39xOFJWQkwxL2tvWFVmYk00S04ydVFadUdjaUdGNjlCRDJ1S3o1eGdvTwowVTBZNmlFNG9Cek5GUW5hWS9kayt5U1dsQWp2MkgraFBrTGpvZlRGSGlNTmUycUVNaUFaeTZ5cmRkSDY4VjdIClRNRllUQlZQaHIxT0dxZlRmc00vRktmZVhWY1FvMTI1RjBJQm5iWjNSYzRua1pNS0hzczUyWE1DZ1lFQTFQRVkKbGIybDU4blVianRZOFl6Uk1vQVo5aHJXMlhwM3JaZjE0Q0VUQ1dsVXFZdCtRN0NyN3dMQUVjbjdrbFk1RGF3QgpuTXJsZXl3S0crTUEvU0hlN3dQQkpNeDlVUGV4Q3YyRW8xT1loMTk3SGQzSk9zUythWWljemJsYmJqU0RqWXVjCkdSNzIrb1FlMzJjTXhjczJNRlBWcHVibjhjalBQbnZKd0k5aUpGVUNnWUVBMjM3UmNKSEdCTjVFM2FXLzd3ekcKbVBuUm1JSUczeW9UU0U3OFBtbHo2bXE5eTVvcSs5aFpaNE1Fdy9RbWFPMDF5U0xRdEY4QmY2TFN2RFh4QWtkdwpWMm5ra0svWWNhWDd3RHo0eWxwS0cxWTg3TzIwWWtkUXlxdjMybG1lN1JuVDhwcVBDQTRUWDloOWFVaXh6THNoCkplcGkvZFhRWFBWeFoxYXV4YldGL3VzQ2dZRUFxWnhVVWNsYVlYS2dzeUN3YXM0WVAxcEwwM3h6VDR5OTBOYXUKY05USFhnSzQvY2J2VHFsbGVaNCtNSzBxcGRmcDM5cjIrZFdlemVvNUx4YzBUV3Z5TDMxVkZhT1AyYk5CSUpqbwpVbE9ldFkwMitvWVM1NjJZWVdVQVNOandXNnFXY21NV2RlZjFIM3VuUDVqTVVxdlhRTTAxNjVnV2ZiN09YRjJyClNLYXNySFVDZ1lCYmRvL1orN1M3dEZSaDZlamJib2h3WGNDRVd4eXhXT2ZMcHdXNXdXT3dlWWZwWTh4cm5pNzQKdGRObHRoRXM4SHhTaTJudEh3TklLSEVlYmJ4eUh1UG5pQjhaWHBwNEJRNTYxczhjR1Z1ZSszbmVFUzBOTDcxZApQL1ZxUWpySFJrd3V5ckRFV2VCeEhUL0FvVEtEeSt3OTQ2SFM5V1dPTGJvbXQrd3g0NytNdWc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=", + "jwk": "eyJ1c2UiOiJzaWciLCJrdHkiOiJSU0EiLCJraWQiOiI4ZjkyNmIyYjAxZjM4MzUxNzAwMjVhNzhhNGRjYmY2YSIsImFsZyI6IlJTMjU2IiwibiI6InprR214QnpBRjJwSDFEYlpoMlRqMkt2bnZQVU5GZlFrTXlzQm8yZWc1anpkSk5kYWtwMEphUHhoNkZxOTZfeTBVd0lBNjdYeFdHb3kxcW1CRGhpdTVqekdtYW13NVgtYkR3TEdTVldGMEx3QnloMDY3TVFvTHJIcDcwMTJxcFU0LUs4NTJPWExFVWVZUGs4djNGWlNDZWcxV0tFblREX2hKaFVRMWxOY0pNY3cxdlRtR2tqNHpqQVE0QUhQX3RxRERxWmd5THNVZmtjbEQ2N0lUZGZLVWRrRVN5b1Q1U3JxYmxzRXpQWHJvamhZS1hpNzFjak1yVDlFS0JCenhZSVEyOVRaZi1nZU5ya0t4a2xMZTJzTUFMX0VWZkFjdGktc2ZqMkkyeEZKZmQ4aklmX2V0cEJJUllUMTNrYUt0dTJiaTRHYi1XUEstS2hCN1M2cUY4OWZMdyIsImUiOiJBUUFCIiwiZCI6IlhmS3FmdjZuZ3FMZTktdEo5ekY5MVJZc1ZyWDZFV3RreDhmSmxjQWdtbTdUdmpHM3FEdU9henMyYjR0Um9mbTN1MjJZdE1UcG16LTRTMjNuQW1BODNsT0ZsTDJsX1VzNy1rRGx4OGFFVHRrc0tYOVo5VG0tSWRMWlB5NjJKQ29YU3JNcDhzRnMxMENZdUowQ0xHZEhOQXNJcllldUtqMllsT3VZZ1Q5MEk2MmpnUWRlM05oblN2MW42MjJyWVdURE94NFpuNHFqQTRpS1NnMl9sbzNWbDhPRU5PYlMyWlRBb3FmUld0LU9OZEMxWnlWWFV5QkRQeW5lVkdxRWNsdEg2Zzc5SXNpLU04ek8yVTJjVlpVTzdoOE8tVW5mYVRhd2xnei1SQlFpWTY3S05Yc1RRQ3VlZ2FJQU1ZVjZxcjVUU1Ai2odx5iT0xSX3BtMWFpdktyUSIsInAiOiI5X1o5ZUpGTWI5X3E4UlZCTDFfa29YVWZiTTRLTjJ1UVp1R2NpR0Y2OUJEMnVLejV4Z29PMFUwWTZpRTRvQnpORlFuYVlfZGsteVNXbEFqdjJILWhQa0xqb2ZURkhpTU5lMnFFTWlBWnk2eXJkZEg2OFY3SFRNRllUQlZQaHIxT0dxZlRmc01fRktmZVhWY1FvMTI1RjBJQm5iWjNSYzRua1pNS0hzczUyWE0iLCJxIjoiMVBFWWxiMmw1OG5VYmp0WThZelJNb0FaOWhyVzJYcDNyWmYxNENFVENXbFVxWXQtUTdDcjd3TEFFY243a2xZNURhd0JuTXJsZXl3S0ctTUFfU0hlN3dQQkpNeDlVUGV4Q3YyRW8xT1loMTk3SGQzSk9zUy1hWWljemJsYmJqU0RqWXVjR1I3Mi1vUWUzMmNNeGNzMk1GUFZwdWJuOGNqUFBudkp3STlpSkZVIiwiZHAiOiIyMzdSY0pIR0JONUUzYVdfN3d6R21QblJtSUlHM3lvVFNFNzhQbWx6Nm1xOXk1b3EtOWhaWjRNRXdfUW1hTzAxeVNMUXRGOEJmNkxTdkRYeEFrZHdWMm5ra0tfWWNhWDd3RHo0eWxwS0cxWTg3TzIwWWtkUXlxdjMybG1lN1JuVDhwcVBDQTRUWDloOWFVaXh6THNoSmVwaV9kWFFYUFZ4WjFhdXhiV0ZfdXMiLCJkcSI6InFaeFVVY2xhWVhLZ3N5Q3dhczRZUDFwTDAzeHpUNHk5ME5hdWNOVEhYZ0s0X2NidlRxbGxlWjQtTUswcXBkZnAzOXIyLWRXZXplbzVMeGMwVFd2eUwzMVZGYU9QMmJOQklKam9VbE9ldFkwMi1vWVM1NjJZWVdVQVNOandXNnFXY21NV2RlZjFIM3VuUDVqTVVxdlhRTTAxNjVnV2ZiN09YRjJyU0thc3JIVSIsInFpIjoiVzNhUDJmdTB1N1JVWWVubzIyNkljRjNBaEZzY3NWam55NmNGdWNGanNIbUg2V1BNYTU0dS1MWFRaYllSTFBCOFVvdHA3UjhEU0NoeEhtMjhjaDdqNTRnZkdWNmFlQVVPZXRiUEhCbGJudnQ1M2hFdERTLTlYVF8xYWtJNngwWk1Mc3F3eEZuZ2NSMF93S0V5Zzh2c1BlT2gwdlZsamkyNkpyZnNNZU9fakxvIn0=", + "created": "2021-06-15T21:06:54.763937286Z" + }, + "success": true, + "errors": [], + "messages": [] } ``` @@ -188,74 +206,75 @@ Here's an example Cloudflare Worker script which generates tokens that expire in ```javascript // Global variables -const jwkKey = '{PRIVATE-KEY-IN-JWK-FORMAT}' -const keyID = '' -const videoUID = '' +const jwkKey = "{PRIVATE-KEY-IN-JWK-FORMAT}"; +const keyID = ""; +const videoUID = ""; // expiresTimeInS is the expired time in second of the video -const expiresTimeInS = 3600 +const expiresTimeInS = 3600; // Main function -async function streamSignedUrl () { - const encoder = new TextEncoder() - const expiresIn = Math.floor(Date.now() / 1000) + expiresTimeInS - const headers = { - "alg": "RS256", - "kid": keyID - } - const data = { - "sub": videoUID, - "kid": keyID, - "exp": expiresIn, - "accessRules": [ - { - "type": "ip.geoip.country", - "action": "allow", - "country": [ - "GB" - ] - }, - { - "type": "any", - "action": "block" - } - ] - } - - const token = `${objectToBase64url(headers)}.${objectToBase64url(data)}` - - const jwk = JSON.parse(atob(jwkKey)) - - const key = await crypto.subtle.importKey( - "jwk", jwk, - { - name: 'RSASSA-PKCS1-v1_5', - hash: 'SHA-256', - }, - false, [ "sign" ] - ) - - const signature = await crypto.subtle.sign( - { name: 'RSASSA-PKCS1-v1_5' }, key, - encoder.encode(token) - ) - - const signedToken = `${token}.${arrayBufferToBase64Url(signature)}` - - return signedToken +async function streamSignedUrl() { + const encoder = new TextEncoder(); + const expiresIn = Math.floor(Date.now() / 1000) + expiresTimeInS; + const headers = { + alg: "RS256", + kid: keyID, + }; + const data = { + sub: videoUID, + kid: keyID, + exp: expiresIn, + accessRules: [ + { + type: "ip.geoip.country", + action: "allow", + country: ["GB"], + }, + { + type: "any", + action: "block", + }, + ], + }; + + const token = `${objectToBase64url(headers)}.${objectToBase64url(data)}`; + + const jwk = JSON.parse(atob(jwkKey)); + + const key = await crypto.subtle.importKey( + "jwk", + jwk, + { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + }, + false, + ["sign"], + ); + + const signature = await crypto.subtle.sign( + { name: "RSASSA-PKCS1-v1_5" }, + key, + encoder.encode(token), + ); + + const signedToken = `${token}.${arrayBufferToBase64Url(signature)}`; + + return signedToken; } // Utilities functions function arrayBufferToBase64Url(buffer) { - return btoa(String.fromCharCode(...new Uint8Array(buffer))) - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_') + return btoa(String.fromCharCode(...new Uint8Array(buffer))) + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"); } function objectToBase64url(payload) { - return arrayBufferToBase64Url( - new TextEncoder().encode(JSON.stringify(payload)), - ) + return arrayBufferToBase64Url( + new TextEncoder().encode(JSON.stringify(payload)), + ); } ``` @@ -264,7 +283,14 @@ function objectToBase64url(payload) { If you are using the Stream Player, insert the token returned by the Worker in Step 2 in place of the video id: ```html - + ``` If you are using your own player, replace the video id in the manifest url with the `token` value: @@ -292,28 +318,26 @@ curl --request DELETE \ ## Supported Restrictions - - -| Property Name | Description | | -| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | -| exp | Expiration. A unix epoch timestamp after which the token will stop working. Cannot be greater than 24 hours in the future from when the token is signed | | -| nbf | *Not Before* value. A unix epoch timestamp before which the token will not work | | -| downloadable | if true, the token can be used to download the mp4 (assuming the video has downloads enabled) | | -| accessRules | An array that specifies one or more ip and geo restrictions. accessRules are evaluated first-to-last. If a Rule matches, the associated action is applied and no further rules are evaluated. A token may have at most 5 members in the accessRules array. | | +| Property Name | Description | | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | +| exp | Expiration. A unix epoch timestamp after which the token will stop working. Cannot be greater than 24 hours in the future from when the token is signed | | +| nbf | _Not Before_ value. A unix epoch timestamp before which the token will not work | | +| downloadable | if true, the token can be used to download the mp4 (assuming the video has downloads enabled) | | +| accessRules | An array that specifies one or more ip and geo restrictions. accessRules are evaluated first-to-last. If a Rule matches, the associated action is applied and no further rules are evaluated. A token may have at most 5 members in the accessRules array. | | ### accessRules Schema Each accessRule must include 2 required properties: -* `type`: supported values are `any`, `ip.src` and `ip.geoip.country` -* `action`: support values are `allow` and `block` +- `type`: supported values are `any`, `ip.src` and `ip.geoip.country` +- `action`: support values are `allow` and `block` Depending on the rule type, accessRules support 2 additional properties: -* `country`: an array of 2-letter country codes in [ISO 3166-1 Alpha 2](https://www.iso.org/obp/ui/#search) format. -* `ip`: an array of ip ranges. It is recommended to include both IPv4 and IPv6 variants in a rule if possible. Having only a single variant in a rule means that rule will ignore the other variant. For example, an IPv4-based rule will never be applicable to a viewer connecting from an IPv6 address. CIDRs should be preferred over specific IP addresses. Some devices, such as mobile, may change their IP over the course of a view. Video Access Control are evaluated continuously while a video is being viewed. As a result, overly strict IP rules may disrupt playback. +- `country`: an array of 2-letter country codes in [ISO 3166-1 Alpha 2](https://www.iso.org/obp/ui/#search) format. +- `ip`: an array of ip ranges. It is recommended to include both IPv4 and IPv6 variants in a rule if possible. Having only a single variant in a rule means that rule will ignore the other variant. For example, an IPv4-based rule will never be applicable to a viewer connecting from an IPv6 address. CIDRs should be preferred over specific IP addresses. Some devices, such as mobile, may change their IP over the course of a view. Video Access Control are evaluated continuously while a video is being viewed. As a result, overly strict IP rules may disrupt playback. -***Example 1: Block views from a specific country*** +**_Example 1: Block views from a specific country_** ```txt ... @@ -328,7 +352,7 @@ Depending on the rule type, accessRules support 2 additional properties: The first rule matches on country, US, DE, and MX here. When that rule matches, the block action will have the token considered invalid. If the first rule doesn't match, there are no further rules to evaluate. The behavior in this situation is to consider the token valid. -***Example 2: Allow only views from specific country or IPs*** +**_Example 2: Allow only views from specific country or IPs_** ```txt ... @@ -364,10 +388,11 @@ By default, Stream embed codes can be used on any domain. If needed, you can lim In the dashboard, you will see a text box by each video labeled `Enter allowed origin domains separated by commas`. If you click on it, you can list the domains that the Stream embed code should be able to be used on. ` -* `*.badtortilla.com` covers `a.badtortilla.com`, `a.b.badtortilla.com` and does not cover `badtortilla.com` -* `example.com` does not cover [www.example.com](http://www.example.com) or any subdomain of example.com -* `localhost` requires a port if it is not being served over HTTP on port 80 or over HTTPS on port 443 -* There is no path support - `example.com` covers `example.com/\*` + +- `*.badtortilla.com` covers `a.badtortilla.com`, `a.b.badtortilla.com` and does not cover `badtortilla.com` +- `example.com` does not cover [www.example.com](http://www.example.com) or any subdomain of example.com +- `localhost` requires a port if it is not being served over HTTP on port 80 or over HTTPS on port 443 +- There is no path support - `example.com` covers `example.com/\*` You can also control embed limitation programmatically using the Stream API. `uid` in the example below refers to the video id. @@ -379,7 +404,7 @@ curl https://api.cloudflare.com/client/v4/accounts/{account_id}/stream/{video_ui ### Allowed Origins -The Allowed Origins feature lets you specify which origins are allowed for playback. This feature works even if you are using your own video player. When using your own video player, Allowed Origins restricts which domain the HLS/DASH manifests and the video segments can be requested from. +The Allowed Origins feature lets you specify which origins are allowed for playback. This feature works even if you are using your own video player. When using your own video player, Allowed Origins restricts which domain the HLS/DASH manifests and the video segments can be requested from. ### Signed URLs diff --git a/src/content/docs/waiting-room/how-to/json-response.mdx b/src/content/docs/waiting-room/how-to/json-response.mdx index b0dcd58a52d914..679969912ecd1c 100644 --- a/src/content/docs/waiting-room/how-to/json-response.mdx +++ b/src/content/docs/waiting-room/how-to/json-response.mdx @@ -3,7 +3,6 @@ pcx_content_type: how-to title: Get JSON response for mobile and other non-browser traffic sidebar: order: 6 - --- If you need to manage traffic in a non-browser environment such as a mobile app or web app, Cloudflare provides a JSON-friendly waiting room that can be consumed via your API endpoints: @@ -17,8 +16,8 @@ In order to consume the waiting room response in the JSON format, the following To receive a JSON response, you first need to enable that option in your waiting room. -* **Via the dashboard**: When [customizing a waiting room](/waiting-room/how-to/customize-waiting-room/), enable **JSON Response**. -* **Via the API**: When [creating a waiting room](/api/operations/waiting-room-create-waiting-room), set `json_response_enabled` to true. +- **Via the dashboard**: When [customizing a waiting room](/waiting-room/how-to/customize-waiting-room/), enable **JSON Response**. +- **Via the API**: When [creating a waiting room](/api/operations/waiting-room-create-waiting-room), set `json_response_enabled` to true. ## Step 2 – Get JSON data @@ -31,16 +30,16 @@ curl "https://example.com/waitingroom" \ ```json title="Response" { - "cfWaitingRoom": { - "inWaitingRoom": true, - "waitTime": 5, - "waitTimeKnown": true, - "waitTimeFormatted": "5 minutes", - "queueIsFull": false, - "queueAll": false, - "lastUpdated": "2021-08-03T23:46:00.000Z", - "refreshIntervalSeconds": 20 - } + "cfWaitingRoom": { + "inWaitingRoom": true, + "waitTime": 5, + "waitTimeKnown": true, + "waitTimeFormatted": "5 minutes", + "queueIsFull": false, + "queueAll": false, + "lastUpdated": "2021-08-03T23:46:00.000Z", + "refreshIntervalSeconds": 20 + } } ``` @@ -57,50 +56,46 @@ In a browser environment, the page automatically refreshes every `refreshInterva These are some of the places where the JSON-friendly response can be consumed (this list is not exhaustive): 1. In a mobile app traffic - * **Integrate Waiting Room variables** – Create a new template in your mobile app to receive the JSON response. For a full list of these variables, refer to the `json_response_enabled` parameter in the [Cloudflare API docs](/api/operations/waiting-room-create-waiting-room). - * **Allow cookies** – As mentioned above, a waiting room [requires cookies](/waiting-room/reference/waiting-room-cookie/), and your mobile app will need to support cookies. For ease of use, consider using a cookie manager like [CookieJar](https://pkg.go.dev/net/http#CookieJar). - * **Consume JSON data** - Make a request to the Waiting Room endpoint with the `Accept: application/json` header. + + - **Integrate Waiting Room variables** – Create a new template in your mobile app to receive the JSON response. For a full list of these variables, refer to the `json_response_enabled` parameter in the [Cloudflare API docs](/api/operations/waiting-room-create-waiting-room). + - **Allow cookies** – As mentioned above, a waiting room [requires cookies](/waiting-room/reference/waiting-room-cookie/), and your mobile app will need to support cookies. For ease of use, consider using a cookie manager like [CookieJar](https://pkg.go.dev/net/http#CookieJar). + - **Consume JSON data** - Make a request to the Waiting Room endpoint with the `Accept: application/json` header. 2. Inside Cloudflare Workers (or in your own backend service) - * **Integrate Waiting Room variables** – Expect a JSON response in your backend API. For a full list of these variables, refer to the `json_response_enabled` parameter in the [Cloudflare API docs](/api/operations/waiting-room-create-waiting-room). - * **Include cookies in the request header** – As mentioned above, a waiting room [requires cookies](/waiting-room/reference/waiting-room-cookie/), and your backend API will need to support cookies. For ease of use, consider using a cookie manager like [CookieJar](https://pkg.go.dev/net/http#CookieJar). - * **Enable JSON response** - Via the dashboard or via the API. - * **Consume JSON data** - Make a request to the Waiting Room endpoint with the `Accept: application/json` header. + + - **Integrate Waiting Room variables** – Expect a JSON response in your backend API. For a full list of these variables, refer to the `json_response_enabled` parameter in the [Cloudflare API docs](/api/operations/waiting-room-create-waiting-room). + - **Include cookies in the request header** – As mentioned above, a waiting room [requires cookies](/waiting-room/reference/waiting-room-cookie/), and your backend API will need to support cookies. For ease of use, consider using a cookie manager like [CookieJar](https://pkg.go.dev/net/http#CookieJar). + - **Enable JSON response** - Via the dashboard or via the API. + - **Consume JSON data** - Make a request to the Waiting Room endpoint with the `Accept: application/json` header. Here is an example, demonstrating the usage of the waiting room endpoint inside a Worker. The request headers include the necessary `accept` and `cookie` header values that are required by the Waiting Room API. The accept header ensures that a JSON-friendly response is returned, if a user is queued. Otherwise, if the request is sent to the origin, then whatever the response origin returns gets returned back. In this example, a hardcoded `__cfwaitingroom` value is embedded in the cookie field. In a real-life application, however, we expect that a cookie returned by the Waiting Room API is used in each of the subsequent requests to ensure that the user is placed accordingly in the queue and let through to the origin when it is the users turn. ```javascript -const waitingroomSite = 'https://examples.cloudflareworkers.com/waiting-room'; - -async function handleRequest() { - const init = { - headers: { - 'accept': 'application/json', - 'cookie': '__cfwaitingroom=F)J@NcRfUjXnZr4u7x!A%D*G-KaPdSgV' - } - } - - return await fetch(waitingroomSite, init) - .then(response => response.json()) - .then(response => { - if (response.cfWaitingRoom.inWaitingRoom) { - return Response('in waiting room', { 'content-type': 'text/html' }); - } - else { - return new Response(response); - } - }) -} - -addEventListener('fetch', event => { - return event.respondWith(handleRequest()); -}); +const waitingroomSite = "https://examples.cloudflareworkers.com/waiting-room"; + +export default { + async fetch(request, env, ctx) { + const init = { + headers: { + accept: "application/json", + cookie: "__cfwaitingroom=F)J@NcRfUjXnZr4u7x!A%D*G-KaPdSgV", + }, + }; + + return fetch(waitingroomSite, init) + .then((response) => response.json()) + .then((response) => { + if (response.cfWaitingRoom.inWaitingRoom) { + return Response("in waiting room", { "content-type": "text/html" }); + } + return new Response(response); + }); + }, +}; ``` :::note - Only Advanced Waiting Room customers can support JSON-friendly format with their waiting rooms. For more details, refer to our [Plans page](/waiting-room/plans/). - ::: diff --git a/src/content/docs/workers/reference/how-the-cache-works.mdx b/src/content/docs/workers/reference/how-the-cache-works.mdx index ac21a7a0a1ebcc..514e23b4119b15 100644 --- a/src/content/docs/workers/reference/how-the-cache-works.mdx +++ b/src/content/docs/workers/reference/how-the-cache-works.mdx @@ -74,7 +74,7 @@ Assets stored in the cache through [Cache API](/workers/runtime-apis/cache/) ope ## Edge versus browser caching -The browser cache is controlled through the `Cache-Control` header sent in the response to the client (the response passed or promised to `event.respondWith()`). Workers can customize browser cache behavior by setting this header on the response. +The browser cache is controlled through the `Cache-Control` header sent in the response to the client (the `Response` instance return from the handler). Workers can customize browser cache behavior by setting this header on the response. Other means to control Cloudflare’s cache that are not mentioned in this documentation include: Page Rules and Cloudflare cache settings. Refer to the [How to customize Cloudflare’s cache](/cache/concepts/customize-cache/) if you wish to avoid writing JavaScript with still some granularity of control.