Pular para o conteúdo

Turnstile with Workers

Inject Turnstile implicitly into HTML elements using the HTMLRewriter runtime API.

Atualizado em Ver como Markdown
export default {
	async fetch(request, env) {
		const SITE_KEY = env.SITE_KEY; // The Turnstile Sitekey of your widget (pass as env or secret)
		const TURNSTILE_ATTR_NAME = "your_id_to_replace"; // The id of the element to put a Turnstile widget in
		let res = await fetch(request);

		// Instantiate the API to run on specific elements, for example, `head`, `div`
		let newRes = new HTMLRewriter()

			// `.on` attaches the element handler and this allows you to match on element/attributes or to use the specific methods per the API
			.on("head", {
				element(element) {
					// In this case, you are using `append` to add a new script to the `head` element
					element.append(
						`<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>`,
						{ html: true },
					);
				},
			})
			.on("div", {
				element(element) {
					// Add a turnstile widget element into if an element with the id of TURNSTILE_ATTR_NAME is found
					if (element.getAttribute("id") === TURNSTILE_ATTR_NAME) {
						element.append(
							`<div class="cf-turnstile" data-sitekey="${SITE_KEY}"></div>`,
							{ html: true },
						);
					}
				},
			})
			.transform(res);
		return newRes;
	},
};
export default {
	async fetch(request, env): Promise<Response> {
		const SITE_KEY = env.SITE_KEY; // The Turnstile Sitekey of your widget (pass as env or secret)
		const TURNSTILE_ATTR_NAME = "your_id_to_replace"; // The id of the element to put a Turnstile widget in

		let res = await fetch(request);

		// Instantiate the API to run on specific elements, for example, `head`, `div`
		let newRes = new HTMLRewriter()

			// `.on` attaches the element handler and this allows you to match on element/attributes or to use the specific methods per the API
			.on("head", {
				element(element) {
					// In this case, you are using `append` to add a new script to the `head` element
					element.append(
						`<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>`,
						{ html: true },
					);
				},
			})
			.on("div", {
				element(element) {
					// Add a turnstile widget element into if an element with the id of TURNSTILE_ATTR_NAME is found
					if (element.getAttribute("id") === TURNSTILE_ATTR_NAME) {
						element.append(
							`<div class="cf-turnstile" data-sitekey="${SITE_KEY}" data-theme="light"></div>`,
							{ html: true },
						);
					}
				},
			})
			.transform(res);
		return newRes;
	},
} satisfies ExportedHandler<Env>;
import { Hono } from "hono";

interface Env {
	SITE_KEY: string;
	SECRET_KEY: string;
	TURNSTILE_ATTR_NAME?: string;
}

const app = new Hono<{ Bindings: Env }>();

// Middleware to inject Turnstile widget
app.use("*", async (c, next) => {
	const SITE_KEY = c.env.SITE_KEY; // The Turnstile Sitekey from environment
	const TURNSTILE_ATTR_NAME = c.env.TURNSTILE_ATTR_NAME || "your_id_to_replace"; // The target element ID

	// Process the request through the original endpoint
	await next();

	// Only process HTML responses
	const contentType = c.res.headers.get("content-type");
	if (!contentType || !contentType.includes("text/html")) {
		return;
	}

	// Clone the response to make it modifiable
	const originalResponse = c.res;
	const responseBody = await originalResponse.text();

	// Create an HTMLRewriter instance to modify the HTML
	const rewriter = new HTMLRewriter()
		// Add the Turnstile script to the head
		.on("head", {
			element(element) {
				element.append(
					`<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>`,
					{ html: true },
				);
			},
		})
		// Add the Turnstile widget to the target div
		.on("div", {
			element(element) {
				if (element.getAttribute("id") === TURNSTILE_ATTR_NAME) {
					element.append(
						`<div class="cf-turnstile" data-sitekey="${SITE_KEY}" data-theme="light"></div>`,
						{ html: true },
					);
				}
			},
		});

	// Create a new response with the same properties as the original
	const modifiedResponse = new Response(responseBody, {
		status: originalResponse.status,
		statusText: originalResponse.statusText,
		headers: originalResponse.headers,
	});

	// Transform the response using HTMLRewriter
	c.res = rewriter.transform(modifiedResponse);
});

// Handle POST requests for form submission with Turnstile validation
app.post("*", async (c) => {
	const formData = await c.req.formData();
	const token = formData.get("cf-turnstile-response");
	const ip = c.req.header("CF-Connecting-IP");

	// If no token, return an error
	if (!token) {
		return c.text("Missing Turnstile token", 400);
	}

	// Prepare verification data
	const verifyFormData = new FormData();
	verifyFormData.append("secret", c.env.SECRET_KEY || "");
	verifyFormData.append("response", token.toString());
	if (ip) verifyFormData.append("remoteip", ip);

	// Verify the token with Turnstile API
	const verifyResult = await fetch(
		"https://challenges.cloudflare.com/turnstile/v0/siteverify",
		{
			method: "POST",
			body: verifyFormData,
		},
	);

	const outcome = await verifyResult.json<{ success: boolean }>;

	// If verification fails, return an error
	if (!outcome.success) {
		return c.text("The provided Turnstile token was not valid!", 401);
	}

	// If verification succeeds, proceed with the original request
	// You would typically handle the form submission logic here

	// For this example, we'll just send a success response
	return c.text("Form submission successful!");
});

// Default handler for GET requests
app.get("*", async (c) => {
	// Fetch the original content (you'd replace this with your actual content source)
	return await fetch(c.req.raw);
});

export default app;
from workers import WorkerEntrypoint
from pyodide.ffi import create_proxy
from js import HTMLRewriter, fetch

class Default(WorkerEntrypoint):
    async def fetch(self, request):
        site_key = self.env.SITE_KEY
        attr_name = self.env.TURNSTILE_ATTR_NAME
        res = await fetch(request)

        class Append:
            def element(self, element):
                s = '<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>'
                element.append(s, {"html": True})

        class AppendOnID:
            def __init__(self, name):
                self.name = name
            def element(self, element):
                # You are using the `getAttribute` method here to retrieve the `id` or `class` of an element
                if element.getAttribute("id") == self.name:
                    div = f'<div class="cf-turnstile" data-sitekey="{site_key}" data-theme="light"></div>'
                    element.append(div, { "html": True })

        # Instantiate the API to run on specific elements, for example, `head`, `div`
        head = create_proxy(Append())
        div = create_proxy(AppendOnID(attr_name))
        new_res = HTMLRewriter.new().on("head", head).on("div", div).transform(res)

        return new_res
async function handlePost(request, env) {
    const body = await request.formData();
    // Turnstile injects a token in `cf-turnstile-response`.
    const token = body.get('cf-turnstile-response');
    const ip = request.headers.get('CF-Connecting-IP');

    // Validate the token by calling the `/siteverify` API.
    let formData = new FormData();

    // `secret_key` here is the Turnstile Secret key, which should be set using Wrangler secrets
    formData.append('secret', self.env.SECRET_KEY);
    formData.append('response', token);
    formData.append('remoteip', ip); //This is optional.

    const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
    const result = await fetch(url, {
        body: formData,
        method: 'POST',
    });

    const outcome = await result.json();

    if (!outcome.success) {
        return new Response('The provided Turnstile token was not valid!', { status: 401 });
    }
    // The Turnstile token was successfully validated. Proceed with your application logic.
    // Validate login, redirect user, etc.

	// Clone the original request with a new body
    const newRequest = new Request(request, {
        body: request.body, // Reuse the body
        method: request.method,
        headers: request.headers
    });

    return await fetch(newRequest);
}

export default {
	async fetch(request, env) {
		const SITE_KEY = env.SITE_KEY; // The Turnstile Sitekey of your widget (pass as env or secret)
		const TURNSTILE_ATTR_NAME = 'your_id_to_replace'; // The id of the element to put a Turnstile widget in

		let res = await fetch(request)

		if (request.method === 'POST') {
			return handlePost(request, env)
		}

		// Instantiate the API to run on specific elements, for example, `head`, `div`
		let newRes = new HTMLRewriter()
			// `.on` attaches the element handler and this allows you to match on element/attributes or to use the specific methods per the API
			.on('head', {
				element(element) {

					// In this case, you are using `append` to add a new script to the `head` element
					element.append(`<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>`, { html: true });
				},
			})
			.on('div', {
				element(element) {
					// You are using the `getAttribute` method here to retrieve the `id` or `class` of an element
					if (element.getAttribute('id') === <NAME_OF_ATTRIBUTE>) {
						element.append(`<div class="cf-turnstile" data-sitekey="${SITE_KEY}" data-theme="light"></div>`, { html: true });
					}
				},
			})
			.transform(res);
		return newRes
	}
}