From ac865e02e735e4d3cb8f219a94653fd795b0bb04 Mon Sep 17 00:00:00 2001 From: Tiago <70700766+tiagorangel1@users.noreply.github.com> Date: Sat, 12 Apr 2025 14:33:36 +0100 Subject: [PATCH] Add solver --- demo/{ => demo}/index.html | 0 demo/{ => demo}/package.json | 0 demo/{ => demo}/server.js | 0 demo/solver/.gitignore | 34 +++++++++++++ demo/solver/README.md | 15 ++++++ demo/solver/bun.lock | 32 +++++++++++++ demo/solver/index.js | 24 ++++++++++ demo/solver/jsconfig.json | 28 +++++++++++ demo/solver/package.json | 15 ++++++ docs/.vitepress/config.mjs | 3 +- docs/guide/solver.md | 39 +++++++++++++++ solver/README.md | 2 + solver/example.js | 24 ++++++++++ solver/index.js | 92 ++++++++++++++++++++++++++++++++++++ solver/package.json | 24 ++++++++++ 15 files changed, 331 insertions(+), 1 deletion(-) rename demo/{ => demo}/index.html (100%) rename demo/{ => demo}/package.json (100%) rename demo/{ => demo}/server.js (100%) create mode 100644 demo/solver/.gitignore create mode 100644 demo/solver/README.md create mode 100644 demo/solver/bun.lock create mode 100644 demo/solver/index.js create mode 100644 demo/solver/jsconfig.json create mode 100644 demo/solver/package.json create mode 100644 docs/guide/solver.md create mode 100644 solver/README.md create mode 100644 solver/example.js create mode 100644 solver/index.js create mode 100644 solver/package.json diff --git a/demo/index.html b/demo/demo/index.html similarity index 100% rename from demo/index.html rename to demo/demo/index.html diff --git a/demo/package.json b/demo/demo/package.json similarity index 100% rename from demo/package.json rename to demo/demo/package.json diff --git a/demo/server.js b/demo/demo/server.js similarity index 100% rename from demo/server.js rename to demo/demo/server.js diff --git a/demo/solver/.gitignore b/demo/solver/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/demo/solver/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/demo/solver/README.md b/demo/solver/README.md new file mode 100644 index 0000000..3d2fb84 --- /dev/null +++ b/demo/solver/README.md @@ -0,0 +1,15 @@ +# solver + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.js +``` + +This project was created using `bun init` in bun v1.2.9. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/demo/solver/bun.lock b/demo/solver/bun.lock new file mode 100644 index 0000000..1499d88 --- /dev/null +++ b/demo/solver/bun.lock @@ -0,0 +1,32 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "solver", + "dependencies": { + "@cap.js/solver": "latest", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@cap.js/solver": ["@cap.js/solver@0.0.2", "", {}, "sha512-DMEth1+iQgRDHibb4alEeYhV32lRpGVkiIVXk9tS7s8So6CV8Gd1U+/KWqB+k7K6yHUqWzRkVQP7INOLUTyqlA=="], + + "@types/bun": ["@types/bun@1.2.9", "", { "dependencies": { "bun-types": "1.2.9" } }, "sha512-epShhLGQYc4Bv/aceHbmBhOz1XgUnuTZgcxjxk+WXwNyDXavv5QHD1QEFV0FwbTSQtNq6g4ZcV6y0vZakTjswg=="], + + "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "bun-types": ["bun-types@1.2.9", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + } +} diff --git a/demo/solver/index.js b/demo/solver/index.js new file mode 100644 index 0000000..240122b --- /dev/null +++ b/demo/solver/index.js @@ -0,0 +1,24 @@ +import solver from "@cap.js/solver"; + +const CHALLENGES = [ + ["e455cea65e98bc3c36287f43769da211", "dceb"], + ["fb8d25f6abac5aa9b6360051f37e010b", "93f1"], + ["91ef47db578fbeb2565d3f9c82bb7960", "3698"], + ["b7ad7667486a691cda8ef297098f64a7", "d72a"], + ["1aca3fb7cef7a2be0dee563ed4136758", "3b58"], + ["d9ec39af92b430e5a329274d8aa58fa8", "e1d3"], + ["781a3cc9217d73c908a321d3fdabd62f", "22c6"], + ["e37a0752c9ac2f3d2517747fde373ac9", "f6f1"], + ["bba070197569f322beda5b240f639a95", "4751"], + ["89297515aeac646bee9653ba405e0beb", "a7de"], + ["444571a0d5039c15be6141d6cd8434f9", "a783"], + ["ba75f2bf8e9b92cc32caa17237a52d14", "7e30"], + ["22bfc18ba8e3ecee080c5d1ef64ed6e9", "5fcf"], + ["885fb78ff76b4eddd2f5bc04ac5ee673", "93e5"], + ["308758072931bb3b254a7b1ed351d04a", "3e49"], + ["724f89bb167db4b881e1dc7b0949ac8f", "b82e"], + ["8b79506e4630de15be225c18623eff65", "f0e5"], + ["0c21ade6e63a4e37b13cb8b087f31863", "65c9"], +]; + +console.log(await solver(CHALLENGES)); diff --git a/demo/solver/jsconfig.json b/demo/solver/jsconfig.json new file mode 100644 index 0000000..9c62f74 --- /dev/null +++ b/demo/solver/jsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/demo/solver/package.json b/demo/solver/package.json new file mode 100644 index 0000000..613b479 --- /dev/null +++ b/demo/solver/package.json @@ -0,0 +1,15 @@ +{ + "name": "solver", + "module": "index.js", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@cap.js/solver": "latest" + } +} diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index f837d46..6b04bb7 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -33,7 +33,8 @@ export default defineConfig({ text: 'Libraries', items: [ { text: '@cap.js/server', link: '/guide/server.md' }, - { text: '@cap.js/widget', link: '/guide/widget.md' } + { text: '@cap.js/widget', link: '/guide/widget.md' }, + { text: '@cap.js/solver', link: '/guide/solver.md' } ] }, { text: "Demo", link: "https://cap-starter.glitch.me/" }, diff --git a/docs/guide/solver.md b/docs/guide/solver.md new file mode 100644 index 0000000..cbcc5e6 --- /dev/null +++ b/docs/guide/solver.md @@ -0,0 +1,39 @@ +# @cap.js/solver + +`@cap.js/solver` is a standalone library that can be used to solve Cap challenges from the server. It's extremely simple (no dependencies, one single file) yet very fast and efficient. Note that it can only be used with Bun. + +## Installation + +```bash +bun add @cap.js/solver +``` + +## Usage + +```js +import solver from "@cap.js/solver"; + +const CHALLENGES = [ + ["a5b6fda4aaed97cf61d7dd9259f733b5", "d455"], + ["286bcc39249f9ee698314b600c32e40f", "f0ff"], + ["501350aa7c46573cb604284554045703", "4971"], + ["a55c02f3b9b4cd088a5a7ee3d4941c14", "eab7"], + ["5f3362c12e2779f56f4ef75b4494f5e6", "999f"], + /* ... */ +]; + +console.log(await solver(CHALLENGES)); +``` + +**Output:** + +```json +[ + ["a5b6fda4aaed97cf61d7dd9259f733b5", "d455", 67302], + ["286bcc39249f9ee698314b600c32e40f", "f0ff", 64511], + ["501350aa7c46573cb604284554045703", "4971", 40440], + ["a55c02f3b9b4cd088a5a7ee3d4941c14", "eab7", 27959], + ["5f3362c12e2779f56f4ef75b4494f5e6", "999f", 71259], + ... +] +``` diff --git a/solver/README.md b/solver/README.md new file mode 100644 index 0000000..c4754ee --- /dev/null +++ b/solver/README.md @@ -0,0 +1,2 @@ +# Server solver for the Cap CAPTCHA +See [https://cap.tiagorangel.com](https://cap.tiagorangel.com) \ No newline at end of file diff --git a/solver/example.js b/solver/example.js new file mode 100644 index 0000000..c6f707b --- /dev/null +++ b/solver/example.js @@ -0,0 +1,24 @@ +import solver from "./index.js"; + +const CHALLENGES = [ + ["e455cea65e98bc3c36287f43769da211", "dceb"], + ["fb8d25f6abac5aa9b6360051f37e010b", "93f1"], + ["91ef47db578fbeb2565d3f9c82bb7960", "3698"], + ["b7ad7667486a691cda8ef297098f64a7", "d72a"], + ["1aca3fb7cef7a2be0dee563ed4136758", "3b58"], + ["d9ec39af92b430e5a329274d8aa58fa8", "e1d3"], + ["781a3cc9217d73c908a321d3fdabd62f", "22c6"], + ["e37a0752c9ac2f3d2517747fde373ac9", "f6f1"], + ["bba070197569f322beda5b240f639a95", "4751"], + ["89297515aeac646bee9653ba405e0beb", "a7de"], + ["444571a0d5039c15be6141d6cd8434f9", "a783"], + ["ba75f2bf8e9b92cc32caa17237a52d14", "7e30"], + ["22bfc18ba8e3ecee080c5d1ef64ed6e9", "5fcf"], + ["885fb78ff76b4eddd2f5bc04ac5ee673", "93e5"], + ["308758072931bb3b254a7b1ed351d04a", "3e49"], + ["724f89bb167db4b881e1dc7b0949ac8f", "b82e"], + ["8b79506e4630de15be225c18623eff65", "f0e5"], + ["0c21ade6e63a4e37b13cb8b087f31863", "65c9"], +]; + +console.log(await solver(CHALLENGES)); diff --git a/solver/index.js b/solver/index.js new file mode 100644 index 0000000..7d8f160 --- /dev/null +++ b/solver/index.js @@ -0,0 +1,92 @@ +import { Worker } from "worker_threads"; +import os from "os"; + +const workerBlob = new Blob( + [ + ` +import crypto from "crypto"; +import { parentPort, workerData } from "worker_threads"; + +const { salt, target } = workerData; + +const saltBuffer = Buffer.from(salt); +const targetLen = target.length; +const targetBytes = Math.ceil(targetLen / 2); + +let nonce = 0; + +while (true) { + const nonceStr = nonce.toString(); + + const hash = crypto.createHash("sha256"); + + hash.update(saltBuffer); + hash.update(nonceStr); + + const hashBuffer = hash.digest(); + + const hashPrefixHex = hashBuffer.toString("hex", 0, targetBytes); + + if (hashPrefixHex.startsWith(target)) { + parentPort.postMessage({ + nonce: Number(nonce), + salt, + target, + }); + break; + } + + nonce++; +}`, + ], + { + type: "application/typescript", + } +); +const workerUrl = URL.createObjectURL(workerBlob); + +export default function solver(challenges, config) { + const totalChallenges = challenges.length; + const numWorkers = + config?.workerCount || Math.min(totalChallenges, 14, os.cpus().length); + + let challengesProcessed = 0; + let nextChallengeIndex = 0; + let activeWorkers = 0; + const results = []; + + return new Promise((resolve) => { + function startWorker() { + if (nextChallengeIndex < totalChallenges && activeWorkers < numWorkers) { + const currentChallengeIndex = nextChallengeIndex; + const [salt, target] = challenges[currentChallengeIndex]; + nextChallengeIndex++; + activeWorkers++; + + const worker = new Worker(workerUrl, { + workerData: { salt, target, challengeIndex: currentChallengeIndex }, + }); + + worker.on("message", (result) => { + results.push([result.salt, result.target, result.nonce]); + challengesProcessed++; + activeWorkers--; + + if (challengesProcessed === totalChallenges) { + resolve(results); + } else { + startWorker(); + } + }); + } + } + + for (let i = 0; i < numWorkers; i++) { + startWorker(); + } + + if (totalChallenges === 0) { + resolve([]); + } + }); +} diff --git a/solver/package.json b/solver/package.json new file mode 100644 index 0000000..f6d2cad --- /dev/null +++ b/solver/package.json @@ -0,0 +1,24 @@ +{ + "name": "@cap.js/solver", + "version": "0.0.2", + "description": "Server-side solver for Cap's SHA-256 PoW captchas", + "main": "index.js", + "scripts": { + "test": "bun ./example.js", + "npm:publish": "npm publish --access public" + }, + "author": "Tiago", + "license": "AGPL-3.0-or-later", + "repository": { + "type": "git", + "url": "git+https://github.com/tiagorangel1/cap.git" + }, + "keywords": [ + "captcha", + "recaptcha" + ], + "bugs": { + "url": "https://github.com/tiagorangel1/cap/issues" + }, + "homepage": "https://github.com/tiagorangel1/cap#readme" +}