Rust Cloudflare Workers: Turnstile Example
Tags: rust, serverless, webdev
Published: 2023-06-27 15:50:04

---
title: "Rust Cloudflare Workers: Turnstile Example"
published: "true"
description: "Svelte real-time multiplayer game 👋🏽 see who is online by adding presence or awareness to improve multi-user game 🧑🏽 user experience."
tags: "rust, serverless, webdev"
canonical_url: "https://rodneylab.com/rust-cloudflare-workers-turnstile-example/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w5g9unwars2ul18xfe8h.png"
---
## ☁️ Rust Cloudflare Workers

In this post, we step beyond initial setup and the basics of Rust Cloudflare workers, where you write worker code in the Rust language and compile to WASM.  In a <a href="https://rodneylab.com/getting-started-rust-cloudflare-workers/">previous  post, we saw how to set up your system for writing Rust</a>, as well as building out a router for a Rust API.  Here, we look at parsing form data on an incoming API HTTP request and also sending HTTP requests to external servers.

### Turnstile

We will step things up a notch, adding an API route which can be used as a serverless backend for **Turnstile CAPTCHA** challenges.  These are the checks websites add to verify the user is not a bot, and so protect forms, for example, from abuse.  Turnstile is a CAPTCHA offering from Cloudflare, which does not require users to solve puzzles.  This is in contrast to alternatives such as **Arkose MetaKey**, **Google&rsquo;s reCAPTCHA** or **hCaptcha**.  Cloudflare JavaScript code runs code in the browser, which it can use to assess whether the use is a human or bot, and without asking them to count traffic lights.

Like other CAPTCHAs, Cloudflare submits some analysis data from the user browser and responds (to the browser) with code.  A second step, run on your site backend, involves submitting the browser response to Cloudflare, along with a secret site key to get Cloudflare's verdict on whether the visitor is a human or bot.  We focus on this second step here, which can be useful for static or JAMStack websites.

You might want to read a <a href="https://rodneylab.com/getting-started-rust-cloudflare-workers/">previous article on getting started with Rust Cloudflare workers</a>, before carrying on, as this post using that, previous, post&rsquo;s code as a starting point.

## ⚙️ Project Setup

In the previous, getting started with Rust Cloudflare workers tutorial, we built up a basic REST API, based on the worker Rust template</a>.  To start, create a new Git branch in your repo from that tutorial or <a aria-label="get the code from the Rodney Lab Git Hub repo" href="https://github.com/rodneylab/serverless-rust/tree/main/demos/getting-started-rust-cloudflare-workers">get the code from the Rodney Lab GitHub repo</a>, then run `pnpm install` in the project directory.

## 🤖 CAPTCHA API Route

We will start by adding a new CAPTHCA route to the router code, in `src/lib.rs`.  The code will listen for form submissions sent from the frontend.  In a real world application, that `POST` request would contain form field data, filled out by the site visitor, as well as a `cf-turnstile-response` field.  Here, we won&rsquo;t look at business logic for processing the form inputs, so just pull that `cf-turnstile-response` field off the incoming POST request.

```rust
#[event(fetch)]
async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
    log_request(&req);
    let router = Router::new();

    router
        .get("/health_check", |_, _| Response::ok("OK"))
        .post_async("/v0/captcha", |mut req, ctx| async move {
            // Read in inputs
            let turnstile_response = match req.form_data().await {
                Ok(value) => {
                    // Get `cf-turnstile-response` field if it exists
                }
                Err(_) => {
                    return Response::error("Bad request", 400);
                }
            };

            // Prepare Turnstile verification HTTP request


            // Verify and process JSON response

            Response::ok("OK")
        })
        .run(req, env)
        .await
}
```

We call `req.form_data()` to pull form fields off the request.  This returns a <a href="https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html">Rust result</a>, which we match on.  If, for example, the request was sent in the wrong format, and we weren&rsquo;t able to parse it as form data, we would end up in the `Err` arm, and would return a `Bad request` response.  

Assuming the request is well-formed, we next want to try to get the value, for the `cf-turnstile-response` field.

```rust
            let turnstile_response = match req.form_data().await {
                Ok(value) => {
                    match value.get("cf-turnstile-response") {
                        Some(FormEntry::Field(field_value)) => field_value,
                        Some(_) => return Response::error("Bad request", 400),
                        None => return Response::error("Bad request", 400),
                    }
                }
                Err(_) => {
                    return Response::error("Bad request", 400);
                }
            }
```

The form data might be well-formed, but missing the `cf-turnstile-response` field.  <a href="https://doc.rust-lang.org/rust-by-example/std/option.html">Options, in Rust</a>, are used to represent a variable which can have a value of a known type or an alternative, unexpected or invalid value.  `value`, in the code block above, is the identifier we assigned to the form data, and calling `get` on it returns a Rust Option.  That Option will be `None`, if the `cf-turnstile-response` is missing.  When the field exists, it could be a file or field string value.  We only care about the field value, and anything else (represented by the `_` should result in a bad request response).

The code above works just fine, though we can make it a little cleaner using Rust `if` `let` syntax, in place of the inner match:

```rust
            let turnstile_response = match req.form_data().await {
                    if let Some(FormEntry::Field(turnstile_response_value)) =
                        value.get("cf-turnstile-response")
                    {
                        turnstile_response_value
                    } else {
                        return Response::error("Bad request", 400);
                    }
                Err(_) => {
                    return Response::error("Bad request", 400);
                }
            }
```

How does that work? Let&rsquo;s see an example.  We get a thermometer reading. This will be a number (64-bit float) if everything went well.  If the thermometer got unplugged, though, we will get nothing back.  Using an option to represent this, the temperature could be `Some(25.0)`, as an example of a normal reading, or `None` when there is no reading for some reason.

```rust
    let temperature: Option<f64> = get_reading();
 
    if let Some(value) = temperature {
        console_log!("The temperature is {value} degrees")
    } else {
        console_log!("The thermometer broke.")
    }
```
For our `Some(25.0)` example, here, `value` will be `25.0`, and we can print the temperature.  For the `None` case, we have no need for `value`, and just say there is no reading.

Our turnstile example takes it up another level, as we specify we only use the `true` if `value.get("cf-turnstile-response")` is `Some`, **and also** it is of `FormEntry::Field` type.

## ⚽️ Reading Request Headers

Next, we can, optionally, pass the client IP address to Cloudflare in our verification request (we look at that request itself in a moment).  The IP address might be included in an HTTP header on the incoming form request to our API.  We can check for the `CF-Connecting-IP` header and get its value with:

```rust
let client_ip_option = req.headers().get("CF-Connecting-IP").unwrap();
```

The `req.headers().get` function call returns a `Result`, like (`req.form_data()`). &ldquo;`unwrap`&rdquo; here, is a way of handling the result without pattern matching, causing a panic in the event of an error.

Pulling it all together, we can build an HTTP request, to send to Turnstile for the verification result using the `reqwest` crate:

```rust
			// Prepare Turnstile verification HTTP request
            let client = reqwest::Client::new();
 
            let mut body_form_map = HashMap::<&str, String>::new();
            if let Some(value) = client_ip_option {
                body_form_map.insert("remoteip", value);
            };
            body_form_map.insert("response", turnstile_response);
            body_form_map.insert("secret", turnstile_secret);
```

To be able to use `reqwest`, though, we should add it in `Cargo.toml`:

```toml
[dependencies]
reqwest = { version = "0.11.18", features = ["json"]}
serde = "1"
worker = "0.0.17"
```

Of course, we have only prepared the request, and not sent it!  Here is the missing code:

```rust
			// Prepare Turnstile verification HTTP request
            let client = reqwest::Client::new();
 
            let mut body_form_map = HashMap::<&str, String>::new();
            if let Some(value) = client_ip_option {
                body_form_map.insert("remoteip", value);
            };
            body_form_map.insert("response", turnstile_response);
            body_form_map.insert("secret", turnstile_secret);
 
            // Verify and process JSON response
            match client
                .post("https://challenges.cloudflare.com/turnstile/v0/siteverify")
                .form(&body_form_map)
                .send()
                .await
            {
                Ok(value) => match value.json::<TurnstileVerifyResponse>().await {
                    Ok(data) => {
                        let TurnstileVerifyResponse { success } = data;
                        console_log!("Turnstile verified: {success}");
                        Ok(Response::ok("OK")?.with_headers(headers))
                    }
                    Err(_) => Response::error("Bad request", 400),
                },
                Err(_) => Response::error("Bad gateway", 501),
            }
        })
        .run(req, env)
        .await
}
```

We are sending the data as `POST` form request.  Naturally, <a href="https://docs.rs/reqwest/0.11.18/reqwest/">`reqwest` supports JSON `POST` requests as well as `GET` and other types of HTTP request</a>.


## 🤫 Secrets and Environment Variables

In the previous post, we saw how you can read meta from `Cargo.toml`, made available as environment variables by Rust.  Let&rsquo;s look a little more at environment variable handling.  Wrangler lets you define variables in the `wrangler.toml` file.  If you need to store secret variables, hang on a moment, we will see something designed specifically for secrets in a moment.

We expect visitors to submit forms from a browser, and so we need to <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">cater for CORS requests</a>.  Specifically, our response needs to include the `Access-Control-Allow-Origin` header.  To see an example of storing variables in `wrangler.toml`, we will add a list of allowed CORS origins there.  Update `wrangler.toml`:

```toml
name = "worker-rust"
main = "build/worker/shim.mjs"
compatibility_date = "2023-03-22"

[vars]
CORS_ORIGIN = "http://127.0.0.1:3001,https://example.com"

[build]
command = "cargo install -q worker-build && worker-build --release"
```

We can then access that parameter from Rust code using:

```rust
ctx.var("CORS_ORIGIN")?.to_string()
```

### Secrets

You can push secrets into your Cloudflare production environment from the Terminal:

```shell
pnpm wrangler secret put TURNSTILE_SECRETKEY
```

However, for local development, use a `.dev.vars` file in your project root directory:

```plaintext
TURNSTILE_SECRETKEY="0x11111111111_abcdefghijklmnopqrstu"
```

Then to access this value from your Rust code, use:

```rust
ctx.secret("TURNSTILE_SECRETKEY")?.to_string();
```

## 🧰 CORS Header Utility Function

We will add a utility function, before wrapping up with the final version of the code.  You can add this code in `src/lib.rs` before the `main` function:


```rust
fn cors_response_headers(request_headers: &worker::Headers, cors_origin: &str) -> worker::Headers {
    let mut headers = worker::Headers::new();
    let origin = match request_headers.get("Origin").unwrap() {
        Some(value) => value,
        None => return headers,
    };
    headers
        .set("Access-Control-Allow-Headers", "Content-Type")
        .expect("Unable to set header");
    headers
        .set("Access-Control-Allow-Methods", "POST")
        .expect("Unable to set header");
    headers.set("Vary", "Origin").expect("Unable to set header");
    if cors_origin.split(',').any(|val| val == cors_origin) {
        headers
            .set("Access-Control-Allow-Origin", &origin)
            .expect("Unable to set header");
    }
    headers
        .set("Access-Control-Max-Age", "86400")
        .expect("Unable to set header");
    headers
}
```

The CAPTCHA route code will call this function, passing in the request headers and the comma separated list of allowed CORS origins.  The request headers should include an `Origin` header, which we can check against the list of allowed origins.

Two points to note here are:
- the use of `expect` to handle results; and
- the <a href="https://doc.rust-lang.org/book/ch13-01-closures.html">Rust closure</a> in lines `40`-`44`.

`expect` works like `unwrap` and causes a panic when the result is not OK.  The difference is that it takes an argument, which gives some feedback if it does panic.

`cors_origin.split(',')` in line `40` splits a comma separated list, in a string such as `"http://127.0.0.1:3001,https://example.com"` into a vector of strings  like: `["http://127.0.0.1:3001", "https://example.com"]` and then returns an iterator over that vector.  We can then call the `any` closure on that value, looking for matches, and return `true` if there is a match.  This functional approach provides a cleaner alternative than an explicit loop over the vector elements, in this case.  

## 🖥️ Final Router Code

Pulling everything together, your final version will look something like this:

```rust
#[event(fetch)]
async fn main(req: Request, env: Env, _ctx: Context) -> Result<Response> {
    log_request(&req);
    let router = Router::new();
 
    router
        .get("/health_check", |_, _| Response::ok("OK"))
        .options("/v0/captcha", |req, ctx| {
            let headers =
                cors_response_headers(req.headers(), &ctx.var("CORS_ORIGIN")?.to_string());

            Ok(Response::empty()
                .unwrap()
                .with_headers(headers)
                .with_status(204))
        })
        .post_async("/v0/captcha", |mut req, ctx| async move {
            // Read in inputs
            let client_ip_option = req.headers().get("CF-Connecting-IP").unwrap();
            let turnstile_secret = ctx.secret("TURNSTILE_SECRETKEY")?.to_string();
 
            let headers =
                cors_response_headers(req.headers(), &ctx.var("CORS_ORIGIN")?.to_string());
 
            let turnstile_response = match req.form_data().await {
                Ok(value) => {
                    if let Some(FormEntry::Field(turnstile_response_value)) =
                        value.get("cf-turnstile-response")
                    {
                        turnstile_response_value
                    } else {
                        return Response::error("Bad request", 400);
                    }
                }
                Err(_) => {
                    return Response::error("Bad request", 400);
                }
            };
 
            // Prepare Turnstile verification HTTP request
            let client = reqwest::Client::new();

            let mut body_form_map = HashMap::<&str, String>::new();
            if let Some(value) = client_ip_option {
                body_form_map.insert("remoteip", value);
            };
            body_form_map.insert("response", turnstile_response);
            body_form_map.insert("secret", turnstile_secret);
 
            // Verify and process JSON response
            match client
                .post("https://challenges.cloudflare.com/turnstile/v0/siteverify")
                .form(&body_form_map)
                .send()
                .await
            {
                Ok(value) => match value.json::<TurnstileVerifyResponse>().await {
                    Ok(data) => {
                        let TurnstileVerifyResponse { success } = data;
                        console_log!("Turnstile verified: {success}");
                        Ok(Response::ok("OK")?.with_headers(headers))
                    }
                    Err(_) => Response::error("Bad request", 400),
                },
                Err(_) => Response::error("Bad gateway", 501),
            }
        })
        .run(req, env)
        .await
}
```
Here, we just put everything together, also adding an extra `captcha` route, listening for CORS `OPTIONS` requests. 

## 💯 Rust Cloudflare Workers: Check your Work

![Rust Cloudflare workers:Terminal screen capture shows logs for a request on the /v0/captcha route with text showing Turnstile successfully verified the user.  Environment variables are also listed.  The value for CORS_ORIGIN is listed, while the TURNSTILE_SECRETKEY is hidden.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kn59vpw9t8e4uylbqzpj.png)

To test this out, you might want to <a href="https://rodneylab.com/getting-started-deno-fresh/">spin up a quick frontend using Deno Fresh</a> or <a href="https://rodneylab.com/astro-js-tutorial/">Astro</a>.  You will need to <a href="https://rodneylab.com/astro-comment-form/">configure Turnstile in your Cloudflare account</a> to get a site key and secret key.

## 🙌🏽 Rust Cloudflare Workers: Wrapping Up

We continued our exploration of Rust and Cloudflare workers adding new routes to the API.  More specifically, we have seen:

- how to **parse form data on an incoming HTTP request** using Rust Cloudflare workers;
- how to **use reqwest to make REST requests**  to external servers; and
- examples of **adding response headers** on API endpoints.

The <a aria-label="Open the Astro JS tutorial code in the Rodney Lab Git Hub Repo" href="https://github.com/rodneylab/serverless-rust/tree/main/demos/rust-cloudflare-workers-turnstile-example">Rust Cloudflare Workers Turnstile Example code is in the Rodney Lab GitHub repo</a>.

I hope you found this article useful, and am keen to hear where you will go next, as well as potential improvements, both to the Rust code and explanations above.  Also, let me know if you would like to see another edition in this series, and which feature would be interesting to explore.

## 🙏🏽 Rust Cloudflare Workers: Feedback

Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also, if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a aria-label="@ask Rodney" href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on Twitter and also <a aria-label="Contact Rodney Lab via Telegram" href="https://t.me/askRodney">askRodney on Telegram</a>. Also, see <a aria-label="Get in touch with Rodney Lab" href="https://rodneylab.com/contact/">further ways to get in touch with Rodney Lab</a>. I post regularly on <a aria-label="See posts on Astro" href="https://rodneylab.com/tags/astro/">Astro</a> as well as <a aria-label="See posts on svelte kit" href="https://rodneylab.com/tags/sveltekit/">SvelteKit</a>. Also, <a href="https://newsletter.rodneylab.com/issue/latest-issue">subscribe to the newsletter to keep up-to-date</a> with our latest projects.