├── .eslintrc.json
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── index.js
├── package.json
├── page_samples
├── sample_challenge_page.html
└── sample_nonchallenge.html
├── src
├── humanoidReqHandler.js
├── response.js
├── solver.js
└── ua.text
└── tests
└── main.test.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "es6": true,
4 | "jest": true
5 | },
6 | "extends": "eslint:recommended",
7 | "parserOptions": {
8 | "ecmaVersion": 2017,
9 | "sourceType": "module"
10 | },
11 | "plugins": [
12 | "jest"
13 | ],
14 | "rules": {
15 | "indent": [
16 | "error",
17 | "tab"
18 | ],
19 | "linebreak-style": [
20 | "error",
21 | "unix"
22 | ],
23 | "quotes": [
24 | "error",
25 | "double"
26 | ],
27 | "semi": [
28 | "error",
29 | "always"
30 | ]
31 | }
32 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # webstorm
33 | .idea
34 |
35 | # Compiled binary addons (https://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # TypeScript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 | # parcel-bundler cache (https://parceljs.org/)
64 | .cache
65 |
66 | # next.js build output
67 | .next
68 |
69 | # nuxt.js build output
70 | .nuxt
71 |
72 | # vuepress build output
73 | .vuepress/dist
74 |
75 | # Serverless directories
76 | .serverless
77 | package-lock.json
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "10"
4 | before_install:
5 | - npm i -g npm@6.4.1
6 | cache:
7 | directories:
8 | - "node_modules"
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Evyatar Meged
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Humanoid
2 | 
3 | 
4 | 
5 | 
6 |
7 | A Node.js package to bypass WAF anti-bot JS challenges.
8 |
9 | ## About
10 | Humanoid is a Node.js package to solve and bypass CloudFlare (and hopefully in the future - other WAFs' as well) JavaScript anti-bot challenges.
11 | While anti-bot pages are solvable via headless browsers, they are pretty heavy and are usually considered over the top for scraping.
12 | Humanoid can solve these challenges using the Node.js runtime and present the protected HTML page.
13 | The session cookies can also be delegated to other bots to continue scraping causing them to avoid the JS challenges altogether.
14 |
15 | ## Features
16 | * Random browser User-Agent
17 | * Auto-retry on failed challenges
18 | * Highly configurable - hack custom cookies, headers, etc
19 | * Clearing cookies and rotating User-Agent is supported
20 | * Supports decompression of `Brotli` content-encoding. Not supported by Node.js' `request` by default!
21 |
22 |
23 | ## Installation
24 | via npm:
25 | ```
26 | npm install --save humanoid-js
27 | ```
28 |
29 | ## Usage
30 | Basic usage with promises:
31 | ```javascript
32 | const Humanoid = require("humanoid-js");
33 |
34 | let humanoid = new Humanoid();
35 | humanoid.get("https://www.cloudflare-protected.com")
36 | .then(res => {
37 | console.log(res.body) // ...
38 | })
39 | .catch(err => {
40 | console.error(err)
41 | })
42 | ```
43 | Humanoid uses auto-bypass by default. You can override it on instance creation:
44 | ```javascript
45 | let humanoid = new Humanoid(autoBypass=false)
46 |
47 | humanoid.get("https://canyoupwn.me")
48 | .then(res => {
49 | console.log(res.statusCode) // 503
50 | console.log(res.isSessionChallenged) // true
51 | humanoid.bypassJSChallenge(res)
52 | .then(challengeResponse => {
53 | // Note that challengeResponse.isChallengeSolved won't be set to true when doing manual bypassing.
54 | console.log(challengeResponse.body) // ...
55 | })
56 | }
57 | )
58 | .catch(err => {
59 | console.error(err)
60 | })
61 | ```
62 | `async/await` is also supported, and is the preferred way to go:
63 | ```javascript
64 | (async function() {
65 | let humanoid = new Humanoid();
66 | let response = await humanoid.sendRequest("www.cloudflare-protected.com")
67 | console.log(response.body) // ...
68 | }())
69 | ```
70 | ### Humanoid API Methods
71 | ```javascript 1.8
72 | rotateUA() // Replace the currently set user agent with a different one
73 |
74 | clearCookies() // "Set a new, empty cookie jar for the humanoid instance"
75 |
76 | get(url, queryString=undefined, headers=undefined) // Send a GET request to `url`.
77 | // if passed, queryString and headers should be objects
78 |
79 | post(url, postBody=undefined, headers=undefined, dataType=undefined) // Send a POST request to `url`.
80 | // `dataType` should be either "form" or "json" - based on the content type of the POST request.
81 |
82 | sendRequest(url, method=undefined, data=undefined, headers=undefined, dataType=undefined)
83 | // Send a request of method `method` to `url`
84 |
85 | bypassJSChallenge(response) // Bypass the anti-bot JS challenge found in response.body
86 | ```
87 |
88 | ## TODOs
89 | - [ ] Add command line support
90 | * Support a flag to return the cookie jar after challenge solved - for better integration with other tools and scrapers
91 | * Have an option to simply bypass and return the protected HTML
92 | - [ ] Solve other WAFs similar anti-bot challenges
93 | - [ ] Add tests for request sending and challenge solving
94 | - [ ] Add Docker support :whale:
95 |
96 | ## Issues and Contributions
97 | All anti-bot challenges are likely to change in the future. If this is the case, please open an issue explaining
98 | the problem - try to include the target page if possible. I'll do my best to keep the code up to date with
99 | new challenges.
100 | Any and all contributions are welcome - and are highly appreciated.
101 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const request = require("request-promise-native");
2 | const Solver = require("./src/solver");
3 | const HumanoidReqHandler = require("./src/humanoidReqHandler");
4 |
5 |
6 | class Humanoid extends HumanoidReqHandler {
7 | constructor(autoBypass=true, maxRetries=3) {
8 | super();
9 | this._getRandomTimeout = () => Math.floor(Math.random() * (7000 - 5000 + 1)) + 5000;
10 | this.autoBypass = autoBypass;
11 | this.maxRetries = maxRetries;
12 | this.currMaxRetries = maxRetries;
13 | this._resetCurrMaxRetries = () => this.currMaxRetries = this.maxRetries;
14 | }
15 |
16 | async _asyncTimeout(ms) {
17 | return new Promise(resolve => {
18 | setTimeout(resolve, ms);
19 | });
20 | }
21 |
22 | rotateUA() {
23 | super.UA = super._getRandomUA();
24 | }
25 |
26 | clearCookies() {
27 | super.cookieJar = request.jar();
28 | }
29 |
30 | _buildAnswerObject(values) {
31 | let [vc, pass, answer] = [...values];
32 | return {
33 | jschl_vc: vc,
34 | pass: pass,
35 | jschl_answer: answer
36 | }
37 | }
38 |
39 | async get(url, queryString=undefined, headers=undefined) {
40 | return await this.sendRequest(url, "GET", queryString, headers)
41 | }
42 |
43 | async post(url, postBody=undefined, headers=undefined, dataType=undefined) {
44 | return await this.sendRequest(url, "POST", postBody, headers, dataType)
45 | }
46 |
47 | async sendRequest(url, method=undefined, data=undefined, headers=undefined, dataType=undefined) {
48 | let response = await super.sendRequest(url, method, data, headers, dataType);
49 | if (response.isSessionChallenged) {
50 | if (this.autoBypass) {
51 | if (--this.currMaxRetries <= 0) {
52 | this._resetCurrMaxRetries();
53 | throw Error(
54 | `Max retries limit reached. Cannot Solve JavaScript challenge from response:
55 | ${JSON.stringify(response)}`
56 | )
57 | } else {
58 | let challengeResponse = await this.bypassJSChallenge(response);
59 | // If we got a 200, mark challenge and solved and return
60 | challengeResponse.isChallengeSolved = challengeResponse.statusCode === 200;
61 | this._resetCurrMaxRetries();
62 | return challengeResponse;
63 | }
64 | }
65 | }
66 | this._resetCurrMaxRetries()
67 | return response;
68 | }
69 |
70 | async bypassJSChallenge(response) {
71 | let {...solution} = Solver.solveChallenge(response);
72 | let timeout = Solver._extractTimeoutFromScript(response.body) || this._getRandomTimeout();
73 | if (![solution.vc, solution.pass, solution.answer, solution.origin].every(elem => !!elem)) {
74 | throw Error(`Failed to Extract one or more necessary values.
75 | Values obtained:
76 | vc: ${solution.vc}
77 | pass: ${solution.pass}
78 | answer${solution.answer}`)
79 | } else {
80 | // Wait the desired time;
81 | await this._asyncTimeout(timeout);
82 | let answerUrl = `${solution.origin}/cdn-cgi/l/chk_jschl`;
83 | let answerObj = this._buildAnswerObject([solution.vc, solution.pass, solution.answer]);
84 | let headers = super._getRequestHeaders(answerUrl);
85 | headers["Referer"] = response.origin;
86 |
87 | let solvedChallengeRes = await this.get(answerUrl, answerObj, headers);
88 | // All requests that reached here were from a challenged session
89 | solvedChallengeRes.isSessionChallenged = true;
90 |
91 | return solvedChallengeRes;
92 | }
93 | }
94 | }
95 |
96 | module.exports = Humanoid;
97 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "humanoid-js",
3 | "version": "1.0.1",
4 | "description": "Node.js package to bypass WAF anti-bot JavaScript challenges",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/evyatarmeged/Humanoid"
8 | },
9 | "main": "index.js",
10 | "scripts": {
11 | "lint": "eslint index.js src/*.js",
12 | "test": "jest tests",
13 | "coverage": "jest --collectCoverageFrom=tests/**.js --coverage src"
14 | },
15 | "keywords": [
16 | "bot",
17 | "WAF",
18 | "bypass",
19 | "cloudflare",
20 | "anti-bot",
21 | "scrape",
22 | "scraping",
23 | "protected-pages",
24 | "captcha"
25 | ],
26 | "author": "Evyatar Meged",
27 | "homepage": "https://github.com/evyatarmeged/Humanoid",
28 | "license": "MIT",
29 | "dependencies": {
30 | "cheerio": "^1.0.0-rc.2",
31 | "iltorb": "^2.4.0",
32 | "request": "^2.88.0",
33 | "request-promise-native": "^1.0.5",
34 | "safe-eval": "^0.4.1",
35 | "url-parse": "^1.4.3"
36 | },
37 | "devDependencies": {
38 | "eslint": "^5.6.1",
39 | "eslint-config-standard": "^12.0.0",
40 | "eslint-plugin-jest": "^21.25.1",
41 | "jest": "^23.6.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/page_samples/sample_challenge_page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Just a moment...
10 |
21 |
22 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
58 |
59 | This process is automatic. Your browser will redirect to your requested content shortly.
60 | Please allow up to 5 seconds…
61 |
62 |
63 |
68 |
69 |
70 |
71 |
75 | |
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/page_samples/sample_nonchallenge.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/humanoidReqHandler.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const rpn = require("request-promise-native");
3 | const URL = require("url-parse");
4 | const Response = require("./response");
5 | const brotli = require('iltorb');
6 |
7 |
8 | class HumanoidReqHandler {
9 | constructor() {
10 | this.cookieJar = rpn.jar()
11 | this._userAgentList = fs.readFileSync(__dirname + "/ua.text").toString().split("\n");
12 | this.UA = this._getRandomUA(); // Set UserAgent
13 | this.config = { // Set default config values
14 | resolveWithFullResponse: true,
15 | jar: this.cookieJar,
16 | simple: false,
17 | gzip: true,
18 | encoding: null
19 | }
20 | }
21 |
22 | _getRandomUA() {
23 | return this._userAgentList[Math.floor(Math.random() * this._userAgentList.length)];
24 | }
25 |
26 | _parseUrl(url) {
27 | return URL(url);
28 | }
29 |
30 | _getConfForMethod(method, config, data, dataType) {
31 | if (method === "GET") {
32 | config.qs = data
33 | } else {
34 | if (dataType === "form") {
35 | config.form = data;
36 | } else if (dataType === "json") {
37 | config.body = data;
38 | config.json = true;
39 | } else {
40 | throw Error(`Data types must be either "Form" or "JSON" as supported by the request npm package`)
41 | }
42 | }
43 | return config;
44 | }
45 |
46 | isCaptchaInResponse(html) {
47 | return html.indexOf("Attention Required! | Cloudflare") > -1 && html.indexOf("CAPTCHA") > -1
48 | }
49 |
50 | isChallengeInResponse(html) {
51 | return html.indexOf("jschl") > -1 && html.indexOf("DDoS protection by Cloudflare") > -1;
52 | }
53 |
54 | async _decompressBrotli(res) {
55 | res.body = await brotli.decompress(res.body);
56 | return res;
57 | }
58 |
59 | _getRequestHeaders(url) {
60 | let headers = {};
61 | headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
62 | headers["Accept-Encoding"] = "gzip, deflate, br";
63 | headers["Connection"] = "keep-alive";
64 | headers["Host"] = this._parseUrl(url).host;
65 | headers["User-Agent"] = this.UA;
66 |
67 | return headers;
68 | }
69 |
70 | async sendRequest(url, method=undefined, data=undefined, headers=undefined, dataType="form") {
71 | // Sanitize parameters
72 | let parsedURL = this._parseUrl(url);
73 | let isSessionChallenged = false;
74 | headers = headers || this._getRequestHeaders(url);
75 | headers["Host"] = !headers.Host ? parsedURL.host : headers["Host"];
76 | method = method !== undefined ? method.toUpperCase() : "GET";
77 | dataType = dataType.toLowerCase();
78 | // Build configuration
79 | let currConfig = {...this.config};
80 | currConfig.headers = headers;
81 | currConfig.method = method;
82 | currConfig = data !== undefined ? this._getConfForMethod(method, currConfig, data, dataType) : currConfig;
83 |
84 | // Send the request
85 | let res = await rpn(url, currConfig);
86 | // Decompress Brotli content-type if returned (Unsupported natively by `request`)
87 | res = res.headers["content-encoding"] === "br" ? await this._decompressBrotli(res) : res;
88 |
89 | if (dataType === "json") {
90 | res.body = JSON.stringify(res.body);
91 | } else {
92 | res.body = res.body.toString();
93 | }
94 |
95 | if (this.isCaptchaInResponse(res.body)) {
96 | throw Error("CAPTCHA page encountered. Cannot perform bypass.")
97 | }
98 |
99 | if (res.statusCode === 503 && this.isChallengeInResponse(res.body)) {
100 | // Session is definitely challenged
101 | isSessionChallenged = true;
102 | }
103 |
104 | return new Response(
105 | res.statusCode, res.statusMessage,
106 | res.headers, res.body,
107 | parsedURL.host, parsedURL.origin,
108 | res.headers["set-cookie"], isSessionChallenged)
109 | }
110 | }
111 |
112 | module.exports = HumanoidReqHandler;
113 |
--------------------------------------------------------------------------------
/src/response.js:
--------------------------------------------------------------------------------
1 | class Response {
2 | constructor(statusCode, statusMessage, headers, body, host, origin, cookies = null,
3 | isSessionChallenged = false, isChallengeSolved = false) {
4 | this.statusCode = statusCode;
5 | this.statusMessage = statusMessage;
6 | this.headers = headers;
7 | this.body = body;
8 | this.host = host;
9 | this.origin = origin;
10 | this.cookies = cookies; // cf session & clearance cookies
11 | this.isSessionChallenged = isSessionChallenged;
12 | this.isChallengeSolved = isChallengeSolved;
13 | }
14 | }
15 |
16 | module.exports = Response;
17 |
--------------------------------------------------------------------------------
/src/solver.js:
--------------------------------------------------------------------------------
1 | const cheerio = require("cheerio");
2 | const safeEval = require("safe-eval");
3 |
4 |
5 | class Solver {
6 | constructor() {}
7 |
8 | static _extractTimeoutFromScript(html) {
9 | let $ = cheerio.load(html);
10 | let script = $("script").html();
11 | let match = script.match(/,\s[0-9]0{3}\);/g);
12 | if (match) {
13 | match = match[0].replace(/,|\s|\)|;/g, "");
14 | }
15 | return match;
16 | }
17 |
18 | static _extractInputValuesFromHTML(html) {
19 | let $ = cheerio.load(html);
20 | return [$("input[name=jschl_vc]").val(), $("input[name=pass]").val()];
21 | }
22 |
23 | static _extractChallengeFromHTML(html) {
24 | let $ = cheerio.load(html);
25 | let script = $("script");
26 | return script.html();
27 | }
28 |
29 | static _operateOnResult(operator, expr, result) {
30 | switch(operator) {
31 | case "+=":
32 | return result += safeEval(expr);
33 | case "*=":
34 | return result *= safeEval(expr);
35 | case "-=":
36 | return result -= safeEval(expr);
37 | case "/=":
38 | return result /= safeEval(expr);
39 | default:
40 | throw Error("Could not match operator. Cannot solve JS challenge");
41 | }
42 | }
43 |
44 | static _buildAnswer(answerMutations, currResult) {
45 | for (let ans of answerMutations) {
46 | let operator = ans.slice(0,2);
47 | let expr = ans.slice(3);
48 | currResult = this._operateOnResult(operator, expr, currResult);
49 | }
50 | return currResult;
51 | }
52 |
53 | static _parseChallenge(matches) {
54 | // Perform the necessary parsing on both challenge parts
55 | let [challengeInit, challengeMutations] = [...matches];
56 | // Perform the necessary parsing
57 | challengeInit = challengeInit.replace(/[;}]/g, "");
58 | challengeMutations = challengeMutations
59 | .split(";")
60 | .map(s => s.match(/(.=.)?(\(\(!\+).*/g))
61 | .filter(s => s !== null)
62 | .map(s => s[0]);
63 |
64 | return [challengeInit, challengeMutations];
65 | }
66 |
67 | static _matchChallengeFromScript(script) {
68 | let testMatches = script.match(/(.=\+)?(\(\(!\+).*/g); // Match the challenge part
69 | if (testMatches.length === 2) {
70 | return testMatches;
71 | }
72 | throw Error("Failed to match JS challenge with Regular Expressions");
73 | }
74 |
75 | static solveChallenge(response) {
76 | let {html, host, origin} = {html: response.body, host: response.host, origin: response.origin};
77 | let script = this._extractChallengeFromHTML(html);
78 | let [vc, pass] = [...this._extractInputValuesFromHTML(html)];
79 |
80 | try {
81 | // Parse only the actual math challenge parts from the script tag and assign them
82 | let challengeMatches = this._matchChallengeFromScript(script);
83 | let [challengeInit, challengeMutations] = this._parseChallenge(challengeMatches);
84 | let answer = this._buildAnswer(challengeMutations, safeEval(challengeInit));
85 | answer = parseFloat(answer.toFixed(10)) + host.length;
86 |
87 | return {vc: vc, pass: pass, answer: answer, origin: origin};
88 | } catch (err) {
89 | throw Error(`Could not solve or parse JavaScript challenge. Caused due to error:\n${err}`);
90 | }
91 | }
92 | }
93 |
94 | module.exports = Solver;
95 |
--------------------------------------------------------------------------------
/src/ua.text:
--------------------------------------------------------------------------------
1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
2 | Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36
3 | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36
4 | Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36
5 | Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36
6 | Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
7 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
8 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36
9 | Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
10 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36
11 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36
12 | Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36
13 | Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36
14 | Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
15 | Mozilla/5.0 (Linux; Android 6.0.1; SM-G532G Build/MMB29T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.83 Mobile Safari/537.36
16 | Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.109 Safari/537.36
17 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36
18 | Mozilla/5.0 (Linux; Android 6.0; vivo 1713 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36
19 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36
20 | Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.63 Safari/537.36
21 | Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
22 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36
23 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
24 | Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.112 Safari/537.36
25 | Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36
26 | Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.65 Safari/537.36
27 | Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
28 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36
29 | Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.89 Safari/537.36
30 | Mozilla/5.0 (Windows NT 6.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
31 | Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
32 | Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
33 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
34 | Mozilla/5.0 (Windows NT 5.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
35 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36
36 | Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36
37 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36
38 | Mozilla/5.0 (Linux; Android 6.0.1; CPH1607 Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/63.0.3239.111 Mobile Safari/537.36
39 | Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36
40 | Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 Safari/537.36
41 | Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36
42 | Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
43 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36
44 | Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4A Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.116 Mobile Safari/537.36
45 | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36
46 | Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
47 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
48 | Mozilla/5.0 (Linux; Android 6.0; vivo 1606 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36
49 | Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36
--------------------------------------------------------------------------------
/tests/main.test.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const Humanoid = require("../index");
3 | const Response = require("../src/response");
4 | const Solver = require("../src/solver");
5 | const humanoidReqHandler = require("../src/humanoidReqHandler");
6 |
7 |
8 | const testHumanoid = new Humanoid()
9 | const testRequestHandler = new humanoidReqHandler();
10 | const testResponse = new Response();
11 |
12 |
13 | test("Response default values", () => {
14 | expect(testResponse.isChallengeSolved).toBeFalsy();
15 | expect(testResponse.isSessionChallenged).toBeFalsy();
16 | expect(testResponse.cookies).toBeNull();
17 | })
18 |
19 | test("Random User Agent", () => {
20 | expect(testRequestHandler._getRandomUA()).toEqual(expect.stringContaining("Mozilla/5.0"));
21 | })
22 |
23 | test("Parsing URL to object with origin/host/etc", () => {
24 | let url = testRequestHandler._parseUrl("http://www.google.com");
25 | expect(url).toHaveProperty("href");
26 | expect(url).toHaveProperty("origin");
27 | expect(url).toHaveProperty("host");
28 | })
29 |
30 | test("Getting configuration for HTTP GET with data", () => {
31 | let getConf = testRequestHandler._getConfForMethod("GET", {}, {a: 1});
32 | expect(getConf.qs).toEqual({a: 1});
33 | })
34 |
35 |
36 | test("Getting configuration for HTTP POST with data", () => {
37 | let postConfJSON = testRequestHandler._getConfForMethod("POST", {}, {b: 2}, "json");
38 | expect(postConfJSON.body).toEqual({b: 2});
39 | expect(postConfJSON.json).toBeTruthy();
40 | let postConfForm = testRequestHandler._getConfForMethod("POST", {}, {c: 3}, "form");
41 | expect(postConfForm.form).toEqual({c: 3});
42 | expect(postConfForm.json).toBeUndefined();
43 | })
44 |
45 | test("CloudFlare JS challenge in page", () => {
46 | let challengeHTML = fs.readFileSync(`${__dirname}/../page_samples/sample_challenge_page.html`);
47 | expect(testRequestHandler.isChallengeInResponse(challengeHTML)).toBeTruthy();
48 | })
49 |
50 | test("CloudFlare JS challenge not in page", () => {
51 | let noChallengeHTML = fs.readFileSync(`${__dirname}/../page_samples/sample_nonchallenge.html`);
52 | expect(testRequestHandler.isChallengeInResponse(noChallengeHTML)).toBeFalsy();
53 | })
54 |
55 | test("Get request headers", () => {
56 | let headers = testRequestHandler._getRequestHeaders("https://www.google.com");
57 | expect(headers).toMatchObject({
58 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
59 | "Accept-Encoding": "gzip, deflate, br",
60 | "Connection": "keep-alive",
61 | "Host": expect.anything(),
62 | "User-Agent": expect.anything()
63 | })
64 | })
65 |
66 | test("Humanoid default values", () => {
67 | expect(testHumanoid.maxRetries).toBe(3);
68 | expect(testHumanoid.autoBypass).toBeTruthy();
69 | })
70 |
71 | test("Reset current max retries", () => {
72 | testHumanoid.currMaxRetries--;
73 | expect(testHumanoid.currMaxRetries).toBe(2);
74 |
75 | testHumanoid._resetCurrMaxRetries();
76 | expect(testHumanoid.currMaxRetries).toBe(3);
77 | })
78 |
79 | test("Clear cookies from jar", () => {
80 | let emptyJar = testHumanoid.cookieJar._jar.store.idx
81 | testHumanoid.cookieJar._jar.store.idx = "google.com"
82 | expect(emptyJar).not.toEqual(testHumanoid.cookieJar._jar.store.idx)
83 |
84 | testHumanoid.clearCookies();
85 | expect(emptyJar).toEqual(testHumanoid.cookieJar._jar.store.idx)
86 | })
87 |
88 | test("Build answer object 1", () => {
89 | let answerObject = testHumanoid._buildAnswerObject([1.123123, 3.123123123, 2.19283918]);
90 | expect(answerObject).toMatchObject({
91 | jschl_vc: 1.123123,
92 | pass: 3.123123123,
93 | jschl_answer: 2.19283918
94 | })
95 | })
96 |
97 | test("Build answer object 2", () => {
98 | let answerObject = testHumanoid._buildAnswerObject([1, 2, 3]);
99 | expect(answerObject).toMatchObject({
100 | jschl_vc: 1,
101 | pass: 2,
102 | jschl_answer: 3
103 | })
104 | })
105 |
106 | test("Extract timeout from script", () => {
107 | let challengeHTML = fs.readFileSync(`${__dirname}/../page_samples/sample_challenge_page.html`);
108 | let timeout = Solver._extractTimeoutFromScript(challengeHTML);
109 | expect(timeout).toBe("4000");
110 | expect(parseInt(timeout)).toBe(4000);
111 | })
112 |
113 | test("Extract input form values", () => {
114 | let challengeHTML = fs.readFileSync(`${__dirname}/../page_samples/sample_challenge_page.html`);
115 | let [a, b] = Solver._extractInputValuesFromHTML(challengeHTML);
116 | expect(a).not.toBeNull();
117 | expect(b).not.toBeNull();
118 | })
119 |
120 | test("Extract challenge", () => {
121 | let challengeHTML = fs.readFileSync(`${__dirname}/../page_samples/sample_challenge_page.html`);
122 | let chal = Solver._extractChallengeFromHTML(challengeHTML);
123 | expect([chal,chal,chal]).toEqual([
124 | expect.stringContaining("f.action += location.hash"),
125 | expect.stringContaining("var s,t,o,p,b,r,e,a,k,i,n,g,f"),
126 | expect.stringContaining("setTimeout(function(){")
127 | ]);
128 | })
129 |
130 | // TODO: Add tests for request sending/challenge solving. Mock functions as needed
--------------------------------------------------------------------------------