Share resume with humans, not bots

bettercallshao
bettercallshao
Share resume with humans, not bots

Story is, I always wanted to make my resume publicly downloadable, then I can put it on my profile pages etc, but I do not want to get spammed by scammers. I came up with cgar a few years ago, it's kinda complicated and requires kubernetes to host, I promptly stopped using it once my free credit at Linode ran out. Recently I picked up job search again, and I recreated the resume dispenser (github) freely hosted on CloudFlare.

Step 1: CloudFlare Turnstile

I noticed OpenAI is using a human checkers from CloudFlare, and I decided to try it. It's free and quite easy to use and they provide great examples like server side validation (github).

// This is the demo secret key. In production, we recommend
// you store your secret key(s) safely.
const SECRET_KEY = '1x0000000000000000000000000000000AA';

async function handlePost(request) {
	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 endpoint.
	let formData = new FormData();
	formData.append('secret', SECRET_KEY);
	formData.append('response', token);
	formData.append('remoteip', ip);

	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) {
		// ...
	}
}

Comments

  • Remember to add the domains after deploying.

Step 2: CloudFlare Workers

Let's back off a little for the big picture with some reductionist logic.

  1. We need to host a file (resume) for download. By itself, it's trivial, we can just put it on github.
  2. We need to check for human, this dictates that we provide a page with a CAPTCHA. Once the human passes the capture, we authorize them to download.
  3. As the host serving the download, how do I know if the CAPTCHA is successful? Typically we put the CAPTCHA on the login page, once login is successful, the user will then request for download with a signed token.
  4. We are a public link (resume), user login is not possible, so we can give out signed tokens to anyone passing the CAPTCHA.

This is an acceptable solution, we can implement a get and a post, with following spec

  • GET: If signed token is valid, return the file. If sign token is missing or invalid, return the CAPTCHA form.
  • POST: Validate CAPTCHA form and return a signed token as a cookie.

I spotted that the token part can be omitted if we modify the spec

  • GET: Return the CAPTCHA form.
  • POST: Validate CAPTCHA form and return the file.

I am not sure if it breaks any laws of the internet to serve a file from a post call, but well. My favorite free hosting service used to be netlify, netlify also provides workers, but with one deadly weakness: the worker hooks are wrapped and can only use JSON as input / output, meaning it's impossible to initiate a download on the browser. I could return the file content base64 encoded and then have some client side logic to decode it (there are a bunch of other tricks we can do if commit to a full blown frontend), but my new favorite CloudFlare workers are versatile and saves me the trouble. It also comes with a great server side example for Turnstile (github).

Comments on modifying the example (github)

  • wrangler dev is great, it sets up a local server for testing.
  • Use env.TURNSTILE_SECRET to fetch secrets in code.
  • Use wrangler secret put TURNSTILE_SECRET to put secrets on CloudFlare.
  • Use .dev.vars to provide secrets for local server.
  • "1x0000000000000000000000000000000AA" is a special secret key in Turnstile for dev.
  • wrangler publish guess what this does.
  • Modify the HTML page with the correct Turnstile public key.

Step 3: CloudFlare Asset & KV

We are almost done! Now the control structure is in place, we just need to serve the file (it's CloudFlare, if it does one thing, this is the thing), I found a good example (github)

import { getAssetFromKV } from '@cloudflare/kv-asset-handler';
import manifestJSON from '__STATIC_CONTENT_MANIFEST';
const assetManifest = JSON.parse(manifestJSON);

export default {
  async fetch(request, env, ctx) {
    try {
      // Add logic to decide whether to serve an asset or run your original Worker code
      return await getAssetFromKV(
        {
          request,
          waitUntil: ctx.waitUntil.bind(ctx),
        },
        {
          ASSET_NAMESPACE: env.__STATIC_CONTENT,
          ASSET_MANIFEST: assetManifest,
        }
      );
    } catch (e) {
      let pathname = new URL(request.url).pathname;
      return new Response(`"${pathname}" not found`, {
        status: 404,
        statusText: 'not found',
      });
    }
  },
};

Comments on modifying the example (github)

  • Put assets in ./public folder. Note I didn't put the assets on github, because it defeats the purpose of protecting them with a human checker.
  • getAssetFromKV does the heavy lifting for us, we just need to provide it with a request object that has a URL that has a path that maps to an asset. Note I had to recreate the request object with GET as method.
  • Our assets are cached in KV, which is database. It means we can go on CloudFlare console and manually modify the file served (until it is overriden by the next deployment).