cmd/anubis: configurable difficulty per-bot rule (#53)

Closes #30

Introduces the "challenge" field in bot rule definitions:

```json
{
  "name": "generic-bot-catchall",
  "user_agent_regex": "(?i:bot|crawler)",
  "action": "CHALLENGE",
  "challenge": {
    "difficulty": 16,
    "report_as": 4,
    "algorithm": "slow"
  }
}
```

This makes Anubis return a challenge page for every user agent with
"bot" or "crawler" in it (case-insensitively) with difficulty 16 using
the old "slow" algorithm but reporting in the client as difficulty 4.

This is useful when you want to make certain clients in particular
suffer.

Additional validation and testing logic has been added to make sure
that users do not define "impossible" challenge settings.

If no algorithm is specified, Anubis defaults to the "fast" algorithm.

Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
Xe Iaso 2025-03-21 13:48:00 -04:00 committed by GitHub
parent 90049001e9
commit d3e509517c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 311 additions and 46 deletions

View file

@ -1,5 +1,11 @@
import { process } from './proof-of-work.mjs';
import { testVideo } from './video.mjs';
import processFast from "./proof-of-work.mjs";
import processSlow from "./proof-of-work-slow.mjs";
import { testVideo } from "./video.mjs";
const algorithms = {
"fast": processFast,
"slow": processSlow,
}
// from Xeact
const u = (url = "", params = {}) => {
@ -37,7 +43,7 @@ const imageURL = (mood, cacheBuster) =>
status.innerHTML = 'Calculating...';
const { challenge, difficulty } = await fetch("/.within.website/x/cmd/anubis/api/make-challenge", { method: "POST" })
const { challenge, rules } = await fetch("/.within.website/x/cmd/anubis/api/make-challenge", { method: "POST" })
.then(r => {
if (!r.ok) {
throw new Error("Failed to fetch config");
@ -47,16 +53,26 @@ const imageURL = (mood, cacheBuster) =>
.catch(err => {
title.innerHTML = "Oh no!";
status.innerHTML = `Failed to fetch config: ${err.message}`;
image.src = imageURL("sad");
image.src = imageURL("sad", anubisVersion);
spinner.innerHTML = "";
spinner.style.display = "none";
throw err;
});
status.innerHTML = `Calculating...<br/>Difficulty: ${difficulty}`;
const process = algorithms[rules.algorithm];
if (!process) {
title.innerHTML = "Oh no!";
status.innerHTML = `Failed to resolve check algorithm. You may want to reload the page.`;
image.src = imageURL("sad", anubisVersion);
spinner.innerHTML = "";
spinner.style.display = "none";
return;
}
status.innerHTML = `Calculating...<br/>Difficulty: ${rules.report_as}`;
const t0 = Date.now();
const { hash, nonce } = await process(challenge, difficulty);
const { hash, nonce } = await process(challenge, rules.difficulty);
const t1 = Date.now();
console.log({ hash, nonce });

View file

@ -0,0 +1,63 @@
// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm
export default function process(data, difficulty = 5, _threads = 1) {
console.debug("slow algo");
return new Promise((resolve, reject) => {
let webWorkerURL = URL.createObjectURL(new Blob([
'(', processTask(), ')()'
], { type: 'application/javascript' }));
let worker = new Worker(webWorkerURL);
worker.onmessage = (event) => {
worker.terminate();
resolve(event.data);
};
worker.onerror = (event) => {
worker.terminate();
reject();
};
worker.postMessage({
data,
difficulty
});
URL.revokeObjectURL(webWorkerURL);
});
}
function processTask() {
return function () {
const sha256 = (text) => {
const encoded = new TextEncoder().encode(text);
return crypto.subtle.digest("SHA-256", encoded.buffer)
.then((result) =>
Array.from(new Uint8Array(result))
.map((c) => c.toString(16).padStart(2, "0"))
.join(""),
);
};
addEventListener('message', async (event) => {
let data = event.data.data;
let difficulty = event.data.difficulty;
let hash;
let nonce = 0;
do {
hash = await sha256(data + nonce++);
} while (hash.substring(0, difficulty) !== Array(difficulty + 1).join('0'));
nonce -= 1; // last nonce was post-incremented
postMessage({
hash,
data,
difficulty,
nonce,
});
});
}.toString();
}

View file

@ -1,6 +1,5 @@
// https://dev.to/ratmd/simple-proof-of-work-in-javascript-3kgm
export function process(data, difficulty = 5, threads = (navigator.hardwareConcurrency || 1)) {
export default function process(data, difficulty = 5, threads = (navigator.hardwareConcurrency || 1)) {
console.debug("fast algo");
return new Promise((resolve, reject) => {
let webWorkerURL = URL.createObjectURL(new Blob([
'(', processTask(), ')()'