diff --git a/src/content/docs/workers/examples/basic-auth.mdx b/src/content/docs/workers/examples/basic-auth.mdx index c00c752e37f078..93303e801f4cf9 100644 --- a/src/content/docs/workers/examples/basic-auth.mdx +++ b/src/content/docs/workers/examples/basic-auth.mdx @@ -7,6 +7,7 @@ tags: languages: - JavaScript - TypeScript + - Rust preview: - true pcx_content_type: example @@ -258,4 +259,75 @@ export default { } satisfies ExportedHandler; ``` - + +```rs +use base64::prelude::*; +use worker::*; + +#[event(fetch)] +async fn fetch(req: Request, env: Env, _ctx: Context) -> Result { + let basic_user = "admin"; + // You will need an admin password. This should be + // attached to your Worker as an encrypted secret. + // Refer to https://developers.cloudflare.com/workers/configuration/secrets/ + let basic_pass = match env.secret("PASSWORD") { + Ok(s) => s.to_string(), + Err(_) => "password".to_string(), + }; + let url = req.url()?; + + match url.path() { + "/" => Response::ok("Anyone can access the homepage."), + // Invalidate the "Authorization" header by returning a HTTP 401. + // We do not send a "WWW-Authenticate" header, as this would trigger + // a popup in the browser, immediately asking for credentials again. + "/logout" => Response::error("Logged out.", 401), + "/admin" => { + // The "Authorization" header is sent when authenticated. + let authorization = req.headers().get("Authorization")?; + if authorization == None { + let mut headers = Headers::new(); + // Prompts the user for credentials. + headers.set( + "WWW-Authenticate", + "Basic realm='my scope', charset='UTF-8'", + )?; + return Ok(Response::error("You need to login.", 401)?.with_headers(headers)); + } + let authorization = authorization.unwrap(); + let auth: Vec<&str> = authorization.split(" ").collect(); + let scheme = auth[0]; + let encoded = auth[1]; + + // The Authorization header must start with Basic, followed by a space. + if encoded == "" || scheme != "Basic" { + return Response::error("Malformed authorization header.", 400); + } + + let buff = BASE64_STANDARD.decode(encoded).unwrap(); + let credentials = String::from_utf8_lossy(&buff); + // The username & password are split by the first colon. + //=> example: "username:password" + let credentials: Vec<&str> = credentials.split(':').collect(); + let user = credentials[0]; + let pass = credentials[1]; + + if user != basic_user || pass != basic_pass { + let mut headers = Headers::new(); + // Prompts the user for credentials. + headers.set( + "WWW-Authenticate", + "Basic realm='my scope', charset='UTF-8'", + )?; + return Ok(Response::error("You need to login.", 401)?.with_headers(headers)); + } + + let mut headers = Headers::new(); + headers.set("Cache-Control", "no-store")?; + Ok(Response::ok("🎉 You have private access!")?.with_headers(headers)) + } + _ => Response::error("Not Found.", 404), + } +} +``` + \ No newline at end of file diff --git a/src/content/docs/workers/examples/cors-header-proxy.mdx b/src/content/docs/workers/examples/cors-header-proxy.mdx index ea2016da590be9..f0db16e550324a 100644 --- a/src/content/docs/workers/examples/cors-header-proxy.mdx +++ b/src/content/docs/workers/examples/cors-header-proxy.mdx @@ -8,6 +8,7 @@ languages: - JavaScript - TypeScript - Python + - Rust pcx_content_type: example title: CORS header proxy sidebar: @@ -436,4 +437,128 @@ async def on_fetch(request): return raw_html_response(demo_page) ``` - + +```rs +use std::{borrow::Cow, collections::HashMap}; +use worker::*; + +fn raw_html_response(html: &str) -> Result { + Response::from_html(html) +} +async fn handle_request(req: Request, api_url: &str) -> Result { + let url = req.url().unwrap(); + let mut api_url2 = url + .query_pairs() + .find(|x| x.0 == Cow::Borrowed("apiurl")) + .unwrap() + .1 + .to_string(); + if api_url2 == String::from("") { + api_url2 = api_url.to_string(); + } + let mut request = req.clone_mut()?; + *request.path_mut()? = api_url2.clone(); + if let url::Origin::Tuple(origin, _, _) = Url::parse(&api_url2)?.origin() { + (*request.headers_mut()?).set("Origin", &origin)?; + } + let mut response = Fetch::Request(request).send().await?.cloned()?; + let headers = response.headers_mut(); + if let url::Origin::Tuple(origin, _, _) = url.origin() { + headers.set("Access-Control-Allow-Origin", &origin)?; + headers.set("Vary", "Origin")?; + } + + Ok(response) +} + +fn handle_options(req: Request, cors_headers: &HashMap<&str, &str>) -> Result { + let headers: Vec<_> = req.headers().keys().collect(); + if [ + "access-control-request-method", + "access-control-request-headers", + "origin", + ] + .iter() + .all(|i| headers.contains(&i.to_string())) + { + let mut headers = Headers::new(); + for (k, v) in cors_headers.iter() { + headers.set(k, v)?; + } + return Ok(Response::empty()?.with_headers(headers)); + } + Response::empty() +} +#[event(fetch)] +async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { + let cors_headers = HashMap::from([ + ("Access-Control-Allow-Origin", "*"), + ("Access-Control-Allow-Methods", "GET,HEAD,POST,OPTIONS"), + ("Access-Control-Max-Age", "86400"), + ]); + let api_url = "https://examples.cloudflareworkers.com/demos/demoapi"; + let proxy_endpoint = "/corsproxy/"; + let demo_page = format!( + r#" + + + +

API GET without CORS Proxy

+ Shows TypeError: Failed to fetch since CORS is misconfigured +

+ Waiting +

API GET with CORS Proxy

+

+ Waiting +

API POST with CORS Proxy + Preflight

+

+ Waiting + + + + "# + ); + + if req.url()?.path().starts_with(proxy_endpoint) { + match req.method() { + Method::Options => return handle_options(req, &cors_headers), + Method::Get | Method::Head | Method::Post => return handle_request(req, api_url).await, + _ => return Response::error("Method Not Allowed", 405), + } + } + raw_html_response(&demo_page) +} +``` + \ No newline at end of file diff --git a/src/content/docs/workers/examples/logging-headers.mdx b/src/content/docs/workers/examples/logging-headers.mdx index 729fa8fdb63d37..9a88fd2e1f2324 100644 --- a/src/content/docs/workers/examples/logging-headers.mdx +++ b/src/content/docs/workers/examples/logging-headers.mdx @@ -6,6 +6,7 @@ tags: - Headers languages: - JavaScript + - Rust - TypeScript - Python preview: @@ -51,6 +52,16 @@ async def on_fetch(request): return Response.new('Hello world') ``` + +```rs +use worker::*; + +#[event(fetch)] +async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { + console_log!("{:?}", req.headers()); + Response::ok("hello world") +} +``` --- @@ -142,4 +153,4 @@ Request headers: { "cf-ipcountry": "US", // ... }" -``` +``` \ No newline at end of file diff --git a/src/content/docs/workers/examples/security-headers.mdx b/src/content/docs/workers/examples/security-headers.mdx index 4dc5b9fb6c4773..0cb2322dda5d92 100644 --- a/src/content/docs/workers/examples/security-headers.mdx +++ b/src/content/docs/workers/examples/security-headers.mdx @@ -10,6 +10,7 @@ languages: - JavaScript - TypeScript - Python + - Rust pcx_content_type: example title: Set security headers sidebar: @@ -250,4 +251,75 @@ async def on_fetch(request): return Response.new(res.body, status=res.status, statusText=res.statusText, headers=new_headers) ``` - + +```rs +use std::collections::HashMap; +use worker::*; + +#[event(fetch)] +async fn fetch(req: Request, _env: Env, _ctx: Context) -> Result { + let default_security_headers = HashMap::from([ + //Secure your application with Content-Security-Policy headers. + //Enabling these headers will permit content from a trusted domain and all its subdomains. + //@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + ( + "Content-Security-Policy", + "default-src 'self' example.com *.example.com", + ), + //You can also set Strict-Transport-Security headers. + //These are not automatically set because your website might get added to Chrome's HSTS preload list. + //Here's the code if you want to apply it: + ( + "Strict-Transport-Security", + "max-age=63072000; includeSubDomains; preload", + ), + //Permissions-Policy header provides the ability to allow or deny the use of browser features, such as opting out of FLoC - which you can use below: + ("Permissions-Policy", "interest-cohort=()"), + //X-XSS-Protection header prevents a page from loading if an XSS attack is detected. + //@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection + ("X-XSS-Protection", "0"), + //X-Frame-Options header prevents click-jacking attacks. + //@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + ("X-Frame-Options", "DENY"), + //X-Content-Type-Options header prevents MIME-sniffing. + //@see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + ("X-Content-Type-Options", "nosniff"), + ("Referrer-Policy", "strict-origin-when-cross-origin"), + ( + "Cross-Origin-Embedder-Policy", + "require-corp; report-to='default';", + ), + ( + "Cross-Origin-Opener-Policy", + "same-site; report-to='default';", + ), + ("Cross-Origin-Resource-Policy", "same-site"), + ]); + let blocked_headers = ["Public-Key-Pins", "X-Powered-By", "X-AspNet-Version"]; + let tls = req.cf().unwrap().tls_version(); + let res = Fetch::Request(req).send().await?; + let mut new_headers = res.headers().clone(); + + // This sets the headers for HTML responses + if Some(String::from("text/html")) == new_headers.get("Content-Type")? { + return Ok(Response::from_body(res.body().clone())? + .with_headers(new_headers) + .with_status(res.status_code())); + } + for (k, v) in default_security_headers { + new_headers.set(k, v)?; + } + + for k in blocked_headers { + new_headers.delete(k)?; + } + + if !vec!["TLSv1.2", "TLSv1.3"].contains(&tls.as_str()) { + return Response::error("You need to use TLS version 1.2 or higher.", 400); + } + Ok(Response::from_body(res.body().clone())? + .with_headers(new_headers) + .with_status(res.status_code())) +} +``` + \ No newline at end of file diff --git a/src/content/docs/workers/examples/websockets.mdx b/src/content/docs/workers/examples/websockets.mdx index 26e2d98c1f42d4..3035f7c93624d2 100644 --- a/src/content/docs/workers/examples/websockets.mdx +++ b/src/content/docs/workers/examples/websockets.mdx @@ -1,20 +1,20 @@ --- type: example -summary: Use the WebSockets API to communicate in real time with your Cloudflare - Workers. +summary: Use the WebSockets API to communicate in real time with your Cloudflare Workers. tags: - WebSockets languages: - JavaScript + - Rust pcx_content_type: example title: Using the WebSockets API sidebar: order: 10000 -description: Use the WebSockets API to communicate in real time with your - Cloudflare Workers. - +description: Use the WebSockets API to communicate in real time with your Cloudflare Workers. --- +import { TabItem, Tabs } from "~/components"; + WebSockets allow you to communicate in real time with your Cloudflare Workers serverless functions. In this guide, you will learn the basics of WebSockets on Cloudflare Workers, both from the perspective of writing WebSocket servers in your Workers functions, as well as connecting to and working with those WebSocket servers as a client. WebSockets are open connections sustained between the client and the origin server. Inside a WebSocket connection, the client and the origin can pass data back and forth without having to reestablish sessions. This makes exchanging data within a WebSocket connection fast. WebSockets are often used for real-time applications such as live chat and gaming. @@ -56,6 +56,7 @@ For more details about creating and working with WebSockets in the client, refer When an incoming WebSocket request reaches the Workers function, it will contain an `Upgrade` header, set to the string value `websocket`. Check for this header before continuing to instantiate a WebSocket: + ```js async function handleRequest(request) { const upgradeHeader = request.headers.get('Upgrade'); @@ -64,9 +65,27 @@ async function handleRequest(request) { } } ``` - + +```rs + +use worker::*; + +#[event(fetch)] +async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { + let upgrade_header = match req.headers().get("Upgrade") { + Some(h) => h.to_str().unwrap(), + None => "", + }; + if upgrade_header != "websocket" { + return worker::Response::error("Expected Upgrade: websocket", 426); + } +} +``` + After you have appropriately checked for the `Upgrade` header, you can create a new instance of `WebSocketPair`, which contains server and client WebSockets. One of these WebSockets should be handled by the Workers function and the other should be returned as part of a `Response` with the [`101` status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101), indicating the request is switching protocols: + + ```js async function handleRequest(request) { const upgradeHeader = request.headers.get('Upgrade'); @@ -84,11 +103,37 @@ async function handleRequest(request) { }); } ``` + +```rs +use worker::*; + +#[event(fetch)] +async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { + let upgrade_header = match req.headers().get("Upgrade") { + Some(h) => h.to_str().unwrap(), + None => "", + }; + if upgrade_header != "websocket" { + return worker::Response::error("Expected Upgrade: websocket", 426); + } + + let ws = WebSocketPair::new()?; + let client = ws.client; + let server = ws.server; + server.accept()?; + + worker::Response::from_websocket(client) +} + +``` + The `WebSocketPair` constructor returns an Object, with the `0` and `1` keys each holding a `WebSocket` instance as its value. It is common to grab the two WebSockets from this pair using [`Object.values`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Object/values) and [ES6 destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), as seen in the below example. In order to begin communicating with the `client` WebSocket in your Worker, call `accept` on the `server` WebSocket. This will tell the Workers runtime that it should listen for WebSocket data and keep the connection open with your `client` WebSocket: + + ```js async function handleRequest(request) { const upgradeHeader = request.headers.get('Upgrade'); @@ -107,9 +152,34 @@ async function handleRequest(request) { }); } ``` + +```rs +use worker::*; + +#[event(fetch)] +async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { + let upgrade_header = match req.headers().get("Upgrade") { + Some(h) => h.to_str().unwrap(), + None => "", + }; + if upgrade_header != "websocket" { + return worker::Response::error("Expected Upgrade: websocket", 426); + } + + let ws = WebSocketPair::new()?; + let client = ws.client; + let server = ws.server; + server.accept()?; + + worker::Response::from_websocket(client) +} + +``` + WebSockets emit a number of [Events](/workers/runtime-apis/websockets/#events) that can be connected to using `addEventListener`. The below example hooks into the `message` event and emits a `console.log` with the data from it: + ```js async function handleRequest(request) { const upgradeHeader = request.headers.get('Upgrade'); @@ -131,6 +201,40 @@ async function handleRequest(request) { }); } ``` + +```rs +use futures::StreamExt; +use worker::*; + +#[event(fetch)] +async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { + let upgrade_header = match req.headers().get("Upgrade") { + Some(h) => h.to_str().unwrap(), + None => "", + }; + if upgrade_header != "websocket" { + return worker::Response::error("Expected Upgrade: websocket", 426); + } + + let ws = WebSocketPair::new()?; + let client = ws.client; + let server = ws.server; + server.accept()?; + + wasm_bindgen_futures::spawn_local(async move { + let mut event_stream = server.events().expect("could not open stream"); + while let Some(event) = event_stream.next().await { + match event.expect("received error in websocket") { + WebsocketEvent::Message(msg) => server.send(&msg.text()).unwrap(), + WebsocketEvent::Close(event) => console_log!("{:?}", event), + } + } + }); + worker::Response::from_websocket(client) +} + +``` + ### Connect to the WebSocket server from a client @@ -196,4 +300,4 @@ async function websocket(url) { ## WebSocket compression -Cloudflare Workers supports WebSocket compression. Refer to [WebSocket Compression](/workers/configuration/compatibility-dates/#websocket-compression) for more information. +Cloudflare Workers supports WebSocket compression. Refer to [WebSocket Compression](/workers/configuration/compatibility-dates/#websocket-compression) for more information. \ No newline at end of file