Compare commits

..

30 Commits

Author SHA1 Message Date
ngosang
3f279e9aa9 Bump version 2.2.4 2022-04-17 09:43:55 +02:00
ngosang
d962e1a14e Detect DDoS-Guard challenge 2022-04-17 09:21:10 +02:00
ngosang
93d8350097 Bump version 2.2.3 2022-04-16 22:29:36 +02:00
ngosang
d34b43e0a8 Fix 2000 ms navigation timeout 2022-04-16 21:39:50 +02:00
ngosang
2bf4dc62da Update README.md (libseccomp2 package in Debian) 2022-04-16 20:47:31 +02:00
termonio
bb0d757755 Update README.md (clarify proxy parameter) (#307)
Clarify that `request.get` will not use the provided proxy when a session is set.
2022-04-16 20:08:48 +02:00
ngosang
fc1fa601eb Update NPM dependencies 2022-04-16 19:19:08 +02:00
ngosang
9b1f8332c7 Disable Cloudflare ban detection 2022-04-16 18:32:58 +02:00
ilike2burnthing
6175fee75a Bump version 2.2.2 (#339) 2022-03-19 04:28:16 +00:00
Harold
bb4fa9cabc Fix ban detection. Resolves #330 (#336) 2022-03-19 04:24:49 +00:00
ngosang
c951ba2523 Bump version 2.2.1 2022-02-06 16:40:03 +01:00
ngosang
6c598d5360 Fix max timeout error in some pages 2022-02-06 16:35:52 +01:00
ngosang
2893f72237 Avoid crashing in NodeJS 17 due to Unhandled promise rejection 2022-02-06 13:31:30 +01:00
ngosang
cd221bbbf1 Improve proxy validation and debug traces 2022-02-06 13:07:11 +01:00
ngosang
68fb96f0d8 Remove @types/puppeteer dependency 2022-02-06 12:53:59 +01:00
ngosang
07724e598f Bump version 2.2.0 2022-01-31 00:20:44 +01:00
ngosang
56fc688517 Increase default BROWSER_TIMEOUT=40000 (40 seconds) 2022-01-30 23:24:15 +01:00
ngosang
0a438358d1 Fix Puppeter deprecation warnings 2022-01-30 23:23:06 +01:00
ngosang
0cbca1fb79 Update base Docker image Alpine 3.15 / NodeJS 16 2022-01-30 23:17:14 +01:00
ngosang
05dcae979c Build precompiled binaries with NodeJS 16 2022-01-30 23:09:28 +01:00
ngosang
fe6cfd75b8 Update Puppeter and other dependencies 2022-01-30 22:49:15 +01:00
ngosang
bb7e82e6c4 Add support for Custom CloudFlare challenge
EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
2022-01-30 21:32:16 +01:00
ngosang
fdd1d245f4 Add support for DDoS-GUARD challenge 2022-01-30 20:36:38 +01:00
ngosang
bc6ac68e52 Bump version 2.1.0 2021-12-12 16:47:33 +01:00
simonfr
a9ab2569bc Add aarch64 to user agents to be replaced (#248)
Co-authored-by: Simon <simon@perols.dev>
2021-12-12 16:46:20 +01:00
ngosang
b1a6ad7688 Fix SOCKSv4 and SOCKSv5 proxy. resolves #214 #220 2021-12-12 14:29:38 +01:00
David Refoua
642d67b927 Remove redundant JSON key (postData) (#242) 2021-12-12 12:38:10 +01:00
ngosang
c4ef6a472e Make test URL configurable with TEST_URL env var. resolves #240 2021-12-12 12:35:05 +01:00
ngosang
a24b665bd1 Bypass new Cloudflare protection 2021-12-12 12:35:05 +01:00
Diego Heras
6576e1908d Update donation links 2021-12-04 23:43:30 +01:00
11 changed files with 3688 additions and 4312 deletions

View File

@@ -1,4 +1,4 @@
FROM --platform=${TARGETPLATFORM:-linux/amd64} node:16-alpine3.14
FROM --platform=${TARGETPLATFORM:-linux/amd64} node:16-alpine3.15
# Print build information
ARG TARGETPLATFORM

View File

@@ -5,14 +5,14 @@
[![GitHub issues](https://img.shields.io/github/issues/FlareSolverr/FlareSolverr)](https://github.com/FlareSolverr/FlareSolverr/issues)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/FlareSolverr/FlareSolverr)](https://github.com/FlareSolverr/FlareSolverr/pulls)
[![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=X5NJLLX5GLTV6&source=url)
[![Donate Buy Me A Coffee](https://img.shields.io/badge/Donate-Buy%20me%20a%20coffee-yellow.svg)](https://www.buymeacoffee.com/ngosang)
[![Donate Bitcoin](https://img.shields.io/badge/Donate-Bitcoin-orange.svg)](https://en.cryptobadges.io/donate/13Hcv77AdnFWEUZ9qUpoPBttQsUT7q9TTh)
[![Donate Bitcoin](https://en.cryptobadges.io/badge/micro/13Hcv77AdnFWEUZ9qUpoPBttQsUT7q9TTh)](https://en.cryptobadges.io/donate/13Hcv77AdnFWEUZ9qUpoPBttQsUT7q9TTh)
[![Donate Ethereum](https://en.cryptobadges.io/badge/micro/0x0D1549BbB00926BF3D92c1A8A58695e982f1BE2E)](https://en.cryptobadges.io/donate/0x0D1549BbB00926BF3D92c1A8A58695e982f1BE2E)
FlareSolverr is a proxy server to bypass Cloudflare protection.
FlareSolverr is a proxy server to bypass Cloudflare and DDoS-GUARD protection.
## How it works
FlareSolverr starts a proxy server and it waits for user requests in an idle state using few resources.
FlareSolverr starts a proxy server, and it waits for user requests in an idle state using few resources.
When some request arrives, it uses [puppeteer](https://github.com/puppeteer/puppeteer) with the
[stealth plugin](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth)
to create a headless browser (Firefox). It opens the URL with user parameters and waits until the Cloudflare challenge
@@ -56,6 +56,10 @@ docker run -d \
ghcr.io/flaresolverr/flaresolverr:latest
```
If your host OS is Debian, make sure `libseccomp2` version is 2.5.x. You can check the version with `sudo apt-cache policy libseccomp2`
and update the package with `sudo apt install libseccomp2=2.5.1-1~bpo10+1` or `sudo apt install libseccomp2=2.5.1-1+deb11u1`.
Remember to restart the Docker daemon and the container after the update.
### Precompiled binaries
This is the recommended way for Windows users.
@@ -66,13 +70,13 @@ This is the recommended way for Windows users.
### From source code
This is the recommended way for macOS users and for developers.
* Install [NodeJS](https://nodejs.org/).
* Install [NodeJS](https://nodejs.org/) 16.
* Clone this repository and open a shell in that path.
* Run `export PUPPETEER_PRODUCT=firefox` (Linux/macOS) or `set PUPPETEER_PRODUCT=firefox` (Windows).
* Run `npm install` command to install FlareSolverr dependencies.
* Run `node node_modules/puppeteer/install.js` to install Firefox.
* Run `npm run build` command to compile TypeScript code.
* Run `npm start` command to start FlareSolverr.
* Run `npm start` command to compile TypeScript code and start FlareSolverr.
If you get errors related to firefox not installed try running `node node_modules/puppeteer/install.js` to install Firefox.
### Systemd service
@@ -104,6 +108,7 @@ This also speeds up the requests since it won't have to launch a new browser ins
Parameter | Notes
|--|--|
session | Optional. The session ID that you want to be assigned to the instance. If isn't set a random UUID will be assigned.
proxy | Optional, default disabled. Eg: `"proxy": {"url": "http://127.0.0.1:8888"}`. You must include the proxy schema in the URL: `http://`, `socks4://` or `socks5://`. Authorization (username/password) is not supported.
#### + `sessions.list`
@@ -141,7 +146,7 @@ session | Optional. Will send the request from and existing browser instance. If
maxTimeout | Optional, default value 60000. Max timeout to solve the challenge in milliseconds.
cookies | Optional. Will be used by the headless browser. Follow [this](https://github.com/puppeteer/puppeteer/blob/v3.3.0/docs/api.md#pagesetcookiecookies) format.
returnOnlyCookies | Optional, default false. Only returns the cookies. Response data, headers and other parts of the response are removed.
proxy | Optional, default disabled. Eg: `"proxy": {"url": "http://127.0.0.1:8888"}`. Authorization (username/password) is not supported.
proxy | Optional, default disabled. Eg: `"proxy": {"url": "http://127.0.0.1:8888"}`. You must include the proxy schema in the URL: `http://`, `socks4://` or `socks5://`. Authorization (username/password) is not supported. (When the `session` parameter is set, the proxy is ignored; a session specific proxy can be set in `sessions.create`.)
:warning: If you want to use Cloudflare clearance cookie in your scripts, make sure you use the FlareSolverr User-Agent too. If they don't match you will see the challenge.
@@ -210,7 +215,7 @@ This is the same as `request.get` but it takes one more param:
Parameter | Notes
|--|--|
postData | Must be a string with `application/x-www-form-urlencoded`. Eg: `postData": "a=b&c=d"`
postData | Must be a string with `application/x-www-form-urlencoded`. Eg: `a=b&c=d`
## Environment variables
@@ -221,7 +226,8 @@ LOG_HTML | false | Only for debugging. If `true` all HTML that passes through th
CAPTCHA_SOLVER | none | Captcha solving method. It is used when a captcha is encountered. See the Captcha Solvers section.
TZ | UTC | Timezone used in the logs and the web browser. Example: `TZ=Europe/London`.
HEADLESS | true | Only for debugging. To run the web browser in headless mode or visible.
BROWSER_TIMEOUT | 30000 | If you are experiencing errors/timeouts because your system is slow, you can try to increase this value. Remember to increase the `maxTimeout` parameter too.
BROWSER_TIMEOUT | 40000 | If you are experiencing errors/timeouts because your system is slow, you can try to increase this value. Remember to increase the `maxTimeout` parameter too.
TEST_URL | https://www.google.com | FlareSolverr makes a request on start to make sure the web browser is working. You can change that URL if it is blocked in your country.
PORT | 8191 | Listening port. You don't need to change this if you are running on Docker.
HOST | 0.0.0.0 | Listening interface. You don't need to change this if you are running on Docker.

View File

@@ -64,8 +64,8 @@ function getFirefoxNightlyVersion() {
if (fs.existsSync('bin')) {
fs.rmSync('bin', { recursive: true })
}
execSync('./node_modules/.bin/pkg -t node14-win-x64,node14-linux-x64 --out-path bin .')
// execSync('./node_modules/.bin/pkg -t node14-win-x64,node14-mac-x64,node14-linux-x64 --out-path bin .')
execSync('./node_modules/.bin/pkg -t node16-win-x64,node16-linux-x64 --out-path bin .')
// execSync('./node_modules/.bin/pkg -t node16-win-x64,node16-mac-x64,node16-linux-x64 --out-path bin .')
// get firefox revision
const revision = await getFirefoxNightlyVersion();

7592
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
{
"name": "flaresolverr",
"version": "2.0.2",
"version": "2.2.4",
"description": "Proxy server to bypass Cloudflare protection.",
"scripts": {
"start": "node ./dist/server.js",
"start": "tsc && node ./dist/server.js",
"build": "tsc",
"dev": "nodemon -e ts --exec ts-node src/server.ts",
"package": "node build-binaries.js",
"package": "tsc && node build-binaries.js",
"test": "jest --runInBand"
},
"author": "Diego Heras (ngosang)",
@@ -20,27 +20,26 @@
},
"dependencies": {
"await-timeout": "^1.1.1",
"body-parser": "^1.19.0",
"body-parser": "^1.20.0",
"console-log-level": "^1.4.1",
"express": "^4.17.1",
"puppeteer": "^3.3.0",
"express": "^4.17.3",
"puppeteer": "^13.5.2",
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/await-timeout": "^0.3.1",
"@types/body-parser": "^1.19.1",
"@types/express": "^4.17.13",
"@types/jest": "^27.0.2",
"@types/node": "^14.17.27",
"@types/puppeteer": "^3.0.6",
"@types/supertest": "^2.0.11",
"@types/jest": "^27.4.1",
"@types/node": "^16.11.27",
"@types/supertest": "^2.0.12",
"@types/uuid": "^8.3.1",
"archiver": "^5.3.0",
"archiver": "^5.3.1",
"nodemon": "^2.0.13",
"pkg": "^5.3.3",
"pkg": "^5.6.0",
"supertest": "^6.1.6",
"ts-jest": "^27.0.7",
"ts-node": "^10.3.0",
"typescript": "^4.4.4"
"ts-jest": "^27.1.4",
"ts-node": "^10.7.0",
"typescript": "^4.6.3"
}
}

View File

@@ -1,6 +1,5 @@
// todo: avoid puppeter objects
import {SetCookie, Headers, HttpMethod} from 'puppeteer'
import {Request, Response} from 'express';
import {Protocol} from "devtools-protocol";
import log from '../services/log'
import {browserRequest, ChallengeResolutionResultT, ChallengeResolutionT} from "../services/solver";
@@ -20,11 +19,11 @@ export interface Proxy {
export interface V1RequestBase {
cmd: string
cookies?: SetCookie[],
cookies?: Protocol.Network.CookieParam[],
maxTimeout?: number
proxy?: Proxy
session: string
headers?: Headers // deprecated v2, not used
headers?: Record<string, string> // deprecated v2, not used
userAgent?: string // deprecated v2, not used
}
@@ -33,7 +32,7 @@ interface V1RequestSession extends V1RequestBase {
export interface V1Request extends V1RequestBase {
url: string
method?: HttpMethod
method?: string
postData?: string
returnOnlyCookies?: boolean
download?: boolean // deprecated v2, not used

View File

@@ -1,4 +1,4 @@
import {Page, Response} from 'puppeteer'
import {Page, HTTPResponse} from 'puppeteer'
import log from "../services/log";
@@ -6,16 +6,35 @@ import log from "../services/log";
* This class contains the logic to solve protections provided by CloudFlare
**/
const BAN_SELECTORS = ['span[data-translate="error"]'];
const CHALLENGE_SELECTORS = ['#trk_jschal_js', '.ray_id', '.attack-box', '#cf-please-wait'];
const CAPTCHA_SELECTORS = ['input[name="cf_captcha_kind"]'];
// the selector '.text-gray-600' is not working well because it can be hidden
// <span style="display: none;" class="text-gray-600" data-translate="error">error code: 1020</span>
const BAN_SELECTORS: string[] = [];
const CHALLENGE_SELECTORS: string[] = [
'#trk_jschal_js', '.ray_id', '.attack-box', '#cf-please-wait', // CloudFlare
'#link-ddg', // DDoS-GUARD
'td.info #js_info' // Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
];
const CAPTCHA_SELECTORS: string[] = [
'input[name="cf_captcha_kind"]'
];
export default async function resolveChallenge(url: string, page: Page, response: Response): Promise<Response> {
export default async function resolveChallenge(url: string, page: Page, response: HTTPResponse): Promise<HTTPResponse> {
// look for challenge and return fast if not detected
if (response.headers().server &&
response.headers().server.startsWith('cloudflare') &&
(response.status() == 403 || response.status() == 503)) {
let cfDetected = response.headers().server &&
(response.headers().server.startsWith('cloudflare') || response.headers().server.startsWith('ddos-guard'));
if (cfDetected) {
if (response.status() == 403 || response.status() == 503) {
cfDetected = true; // Defected CloudFlare and DDoS-GUARD
} else if (response.headers().vary && response.headers().vary.trim() == 'Accept-Encoding,User-Agent' &&
response.headers()['content-encoding'] && response.headers()['content-encoding'].trim() == 'br') {
cfDetected = true; // Detected Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
} else {
cfDetected = false;
}
}
if (cfDetected) {
log.info('Cloudflare detected');
} else {
log.info('Cloudflare not detected');
@@ -23,79 +42,71 @@ export default async function resolveChallenge(url: string, page: Page, response
}
if (await findAnySelector(page, BAN_SELECTORS)) {
throw new Error('Cloudflare has blocked this request. Probably your IP is banned for this site, check in your web browser.')
throw new Error('Cloudflare has blocked this request. Probably your IP is banned for this site, check in your web browser.');
}
// find Cloudflare selectors
let selectorFound = false;
if (response.status() > 400) {
let selector: string = await findAnySelector(page, CHALLENGE_SELECTORS)
if (selector) {
selectorFound = true;
log.debug(`Javascript challenge element '${selector}' detected.`)
log.debug('Waiting for Cloudflare challenge...')
// find Cloudflare selectors
let selector: string = await findAnySelector(page, CHALLENGE_SELECTORS)
if (selector) {
selectorFound = true;
log.debug(`Javascript challenge element '${selector}' detected.`)
log.debug('Waiting for Cloudflare challenge...')
while (true) {
try {
while (true) {
try {
selector = await findAnySelector(page, CHALLENGE_SELECTORS)
if (!selector) {
// solved!
log.debug('Challenge element not found')
break
} else {
log.debug(`Javascript challenge element '${selector}' detected.`)
selector = await findAnySelector(page, CHALLENGE_SELECTORS)
if (!selector) {
// solved!
log.debug('Challenge element not found')
// new Cloudflare Challenge #cf-please-wait
const displayStyle = await page.evaluate((selector) => {
return getComputedStyle(document.querySelector(selector)).getPropertyValue("display");
}, selector);
if (displayStyle == "none") {
// spinner is hidden, could be a captcha or not
log.debug('Challenge element is hidden')
// wait until redirecting disappears
while (true) {
try {
await page.waitForTimeout(1000)
const displayStyle2 = await page.evaluate(() => {
return getComputedStyle(document.querySelector('#cf-spinner-redirecting')).getPropertyValue("display");
});
if (displayStyle2 == "none") {
break // hCaptcha detected
}
} catch (error) {
break // redirection completed
}
}
break
} else {
log.debug(`Javascript challenge element '${selector}' detected.`)
// new Cloudflare Challenge #cf-please-wait
const displayStyle = await page.evaluate((selector) => {
return getComputedStyle(document.querySelector(selector)).getPropertyValue("display");
}, selector);
if (displayStyle == "none") {
// spinner is hidden, could be a captcha or not
log.debug('Challenge element is hidden')
// wait until redirecting disappears
while (true) {
try {
await page.waitFor(1000)
const displayStyle2 = await page.evaluate(() => {
return getComputedStyle(document.querySelector('#cf-spinner-redirecting')).getPropertyValue("display");
});
if (displayStyle2 == "none") {
break // hCaptcha detected
}
} catch (error) {
break // redirection completed
}
}
break
} else {
log.debug('Challenge element is visible')
}
}
log.debug('Found challenge element again')
} catch (error)
{
log.debug("Unexpected error: " + error);
if (!error.toString().includes("Execution context was destroyed")) {
break
log.debug('Challenge element is visible')
}
}
log.debug('Found challenge element again')
log.debug('Waiting for Cloudflare challenge...')
await page.waitFor(1000)
} catch (error)
{
log.debug("Unexpected error: " + error);
if (!error.toString().includes("Execution context was destroyed")) {
break
}
}
log.debug('Validating HTML code...')
} else {
log.debug(`No challenge element detected.`)
log.debug('Waiting for Cloudflare challenge...')
await page.waitForTimeout(1000)
}
log.debug('Validating HTML code...')
} else {
// some sites use cloudflare but there is no challenge
log.debug(`Javascript challenge not detected. Status code: ${response.status()}`);
selectorFound = true;
log.debug(`No challenge element detected.`)
}
// check for CAPTCHA challenge

View File

@@ -39,6 +39,11 @@ process.on('SIGTERM', () => {
process.exit(0)
})
process.on('uncaughtException', function(err) {
// Avoid crashing in NodeJS 17 due to UnhandledPromiseRejectionWarning: Unhandled promise rejection.
log.error(err)
})
validateEnvironmentVariables();
testWebBrowserInstallation().then(() => {

View File

@@ -1,6 +1,7 @@
import {v1 as UUIDv1} from 'uuid'
import * as path from 'path'
import {SetCookie, Browser} from 'puppeteer'
import {Browser} from 'puppeteer'
import {Protocol} from "devtools-protocol";
import log from './log'
import {Proxy} from "../controllers/v1";
@@ -20,7 +21,7 @@ interface SessionsCache {
export interface SessionCreateOptions {
oneTimeSession: boolean
cookies?: SetCookie[],
cookies?: Protocol.Network.CookieParam[],
maxTimeout?: number
proxy?: Proxy
}
@@ -46,8 +47,9 @@ function buildExtraPrefsFirefox(proxy: Proxy): object {
"startup.homepage_welcome_url": "about:blank",
"startup.homepage_welcome_url.additional": "",
// Disable images to speed up load
"permissions.default.image": 2,
// Detected !
// // Disable images to speed up load
// "permissions.default.image": 2,
// Limit content processes to 1
"dom.ipc.processCount": 1
@@ -55,22 +57,43 @@ function buildExtraPrefsFirefox(proxy: Proxy): object {
// proxy.url format => http://<host>:<port>
if (proxy && proxy.url) {
log.debug(`Using proxy: ${proxy.url}`)
const [host, portStr] = proxy.url.replace(/.+:\/\//g, '').split(':');
const port = parseInt(portStr);
if (!host || !portStr || !port) {
throw new Error("Proxy configuration is invalid! Use the format: protocol://ip:port")
}
const proxyPrefs = {
// Proxy configuration
"network.proxy.ftp": host,
"network.proxy.ftp_port": port,
"network.proxy.http": host,
"network.proxy.http_port": port,
"network.proxy.share_proxy_settings": true,
"network.proxy.socks": host,
"network.proxy.socks_port": port,
"network.proxy.socks_remote_dns": true,
"network.proxy.ssl": host,
"network.proxy.ssl_port": port,
"network.proxy.type": 1
"network.proxy.type": 1,
"network.proxy.share_proxy_settings": true
}
if (proxy.url.indexOf("socks") != -1) {
// SOCKSv4 & SOCKSv5
Object.assign(proxyPrefs, {
"network.proxy.socks": host,
"network.proxy.socks_port": port,
"network.proxy.socks_remote_dns": true
});
if (proxy.url.indexOf("socks4") != -1) {
Object.assign(proxyPrefs, {
"network.proxy.socks_version": 4
});
} else {
Object.assign(proxyPrefs, {
"network.proxy.socks_version": 5
});
}
} else {
// HTTP
Object.assign(proxyPrefs, {
"network.proxy.ftp": host,
"network.proxy.ftp_port": port,
"network.proxy.http": host,
"network.proxy.http_port": port,
"network.proxy.ssl": host,
"network.proxy.ssl_port": port
});
}
// merge objects
@@ -93,16 +116,19 @@ export async function testWebBrowserInstallation(): Promise<void> {
log.debug("FlareSolverr user home directory is OK: " + homeDir)
// test web browser
const testUrl = process.env.TEST_URL || "https://www.google.com";
log.debug("Test URL: " + testUrl)
const session = await create(null, {
oneTimeSession: true
})
const page = await session.browser.newPage()
await page.goto("https://www.google.com")
const pageTimeout = Number(process.env.BROWSER_TIMEOUT) || 40000
await page.goto(testUrl, {waitUntil: 'domcontentloaded', timeout: pageTimeout})
webBrowserUserAgent = await page.evaluate(() => navigator.userAgent)
// replace Linux ARM user-agent because it's detected
if (webBrowserUserAgent.toLocaleLowerCase().includes('linux arm')) {
webBrowserUserAgent = webBrowserUserAgent.replace(/linux arm[^;]+;/i, 'Linux x86_64;')
if (["arm", "aarch64"].some(arch => webBrowserUserAgent.toLocaleLowerCase().includes('linux ' + arch))) {
webBrowserUserAgent = webBrowserUserAgent.replace(/linux \w+;/i, 'Linux x86_64;')
}
log.info("FlareSolverr User-Agent: " + webBrowserUserAgent)
@@ -113,6 +139,8 @@ export async function testWebBrowserInstallation(): Promise<void> {
}
export async function create(session: string, options: SessionCreateOptions): Promise<SessionsCacheItem> {
log.debug('Creating new session...')
const sessionId = session || UUIDv1()
// NOTE: cookies can't be set in the session, you need to open the page first
@@ -120,7 +148,7 @@ export async function create(session: string, options: SessionCreateOptions): Pr
const puppeteerOptions: any = {
product: 'firefox',
headless: process.env.HEADLESS !== 'false',
timeout: process.env.BROWSER_TIMEOUT || 30000
timeout: Number(process.env.BROWSER_TIMEOUT) || 40000
}
puppeteerOptions.extraPrefsFirefox = buildExtraPrefsFirefox(options.proxy)

View File

@@ -1,4 +1,4 @@
import {Response, Headers, Page} from 'puppeteer'
import {Page, HTTPResponse} from 'puppeteer'
const Timeout = require('await-timeout');
import log from './log'
@@ -11,7 +11,7 @@ const sessions = require('./sessions')
export interface ChallengeResolutionResultT {
url: string
status: number,
headers?: Headers,
headers?: Record<string, string>,
response: string,
cookies: object[]
userAgent: string
@@ -64,7 +64,7 @@ async function resolveChallenge(params: V1Request, session: SessionsCacheItem):
// go to the page
log.debug(`Navigating to... ${params.url}`)
let response: Response = await gotoPage(params, page);
let response: HTTPResponse = await gotoPage(params, page);
// set cookies
if (params.cookies) {
@@ -89,7 +89,11 @@ async function resolveChallenge(params: V1Request, session: SessionsCacheItem):
// is response is ok
// reload the page to be sure we get the real page
log.debug("Reloading the page")
response = await gotoPage(params, page);
try {
response = await gotoPage(params, page);
} catch (e) {
log.warn("Page not reloaded (do not report!): Cause: " + e.toString())
}
} catch (e) {
status = "error";
@@ -128,15 +132,18 @@ async function resolveChallenge(params: V1Request, session: SessionsCacheItem):
}
}
async function gotoPage(params: V1Request, page: Page): Promise<Response> {
let response: Response;
if (params.method != 'POST') {
response = await page.goto(params.url, {waitUntil: 'domcontentloaded'});
async function gotoPage(params: V1Request, page: Page): Promise<HTTPResponse> {
let pageTimeout = params.maxTimeout / 3;
let response: HTTPResponse
try {
response = await page.goto(params.url, {waitUntil: 'domcontentloaded', timeout: pageTimeout});
} catch (e) {
// retry
response = await page.goto(params.url, {waitUntil: 'domcontentloaded', timeout: pageTimeout});
}
} else {
if (params.method == 'POST') {
// post hack
// first request a page without cloudflare
response = await page.goto(params.url, {waitUntil: 'domcontentloaded'});
await page.setContent(
`
<!DOCTYPE html>
@@ -177,7 +184,7 @@ async function gotoPage(params: V1Request, page: Page): Promise<Response> {
</html>
`
);
await page.waitFor(2000)
await page.waitForTimeout(2000)
try {
await page.waitForNavigation({waitUntil: 'domcontentloaded', timeout: 2000})
} catch (e) {}

View File

@@ -9,11 +9,14 @@ const sessions = require('../services/sessions');
const version: string = 'v' + require('../../package.json').version
const proxyUrl = "http://127.0.0.1:8888"
const proxySocksUrl = "socks5://127.0.0.1:1080"
const googleUrl = "https://www.google.com";
const postUrl = "https://ptsv2.com/t/qv4j3-1634496523";
const cfUrl = "https://pirateiro.com/torrents/?search=harry";
const cfCaptchaUrl = "https://idope.se"
const cfBlockedUrl = "https://www.torrentmafya.org/table.php"
const ddgUrl = "https://anidex.info/";
const ccfUrl = "https://www.muziekfabriek.org";
beforeAll(async () => {
// Init session
@@ -167,6 +170,64 @@ describe("Test '/v1' path", () => {
expect(apiResponse.solution.url).toContain(cfBlockedUrl)
});
test("Cmd 'request.get' should return OK with DDoS-GUARD JS", async () => {
const payload = {
"cmd": "request.get",
"url": ddgUrl
}
const response: Response = await request(app).post("/v1").send(payload);
expect(response.statusCode).toBe(200);
const apiResponse: V1ResponseSolution = response.body;
expect(apiResponse.status).toBe("ok");
expect(apiResponse.message).toBe("");
expect(apiResponse.startTimestamp).toBeGreaterThan(1000);
expect(apiResponse.endTimestamp).toBeGreaterThan(apiResponse.startTimestamp);
expect(apiResponse.version).toBe(version);
const solution = apiResponse.solution;
expect(solution.url).toContain(ddgUrl)
expect(solution.status).toBe(200);
expect(Object.keys(solution.headers).length).toBeGreaterThan(0)
expect(solution.response).toContain("<!DOCTYPE html>")
expect(Object.keys(solution.cookies).length).toBeGreaterThan(0)
expect(solution.userAgent).toContain("Firefox/")
const cfCookie: string = (solution.cookies as any[]).filter(function(cookie) {
return cookie.name == "__ddg1_";
})[0].value
expect(cfCookie.length).toBeGreaterThan(10)
});
test("Cmd 'request.get' should return OK with Custom CloudFlare JS", async () => {
const payload = {
"cmd": "request.get",
"url": ccfUrl
}
const response: Response = await request(app).post("/v1").send(payload);
expect(response.statusCode).toBe(200);
const apiResponse: V1ResponseSolution = response.body;
expect(apiResponse.status).toBe("ok");
expect(apiResponse.message).toBe("");
expect(apiResponse.startTimestamp).toBeGreaterThan(1000);
expect(apiResponse.endTimestamp).toBeGreaterThan(apiResponse.startTimestamp);
expect(apiResponse.version).toBe(version);
const solution = apiResponse.solution;
expect(solution.url).toContain(ccfUrl)
expect(solution.status).toBe(200);
expect(Object.keys(solution.headers).length).toBeGreaterThan(0)
expect(solution.response).toContain("<html><head>")
expect(Object.keys(solution.cookies).length).toBeGreaterThan(0)
expect(solution.userAgent).toContain("Firefox/")
const cfCookie: string = (solution.cookies as any[]).filter(function(cookie) {
return cookie.name == "ct_anti_ddos_key";
})[0].value
expect(cfCookie.length).toBeGreaterThan(10)
});
test("Cmd 'request.get' should return OK with 'cookies' param", async () => {
const payload = {
"cmd": "request.get",
@@ -221,7 +282,7 @@ describe("Test '/v1' path", () => {
expect(solution.userAgent).toBe(null)
});
test("Cmd 'request.get' should return OK with 'proxy' param", async () => {
test("Cmd 'request.get' should return OK with HTTP 'proxy' param", async () => {
/*
To configure TinyProxy in local:
* sudo vim /etc/tinyproxy/tinyproxy.conf
@@ -249,7 +310,7 @@ describe("Test '/v1' path", () => {
});
// todo: credentials are not working
test.skip("Cmd 'request.get' should return OK with 'proxy' param with credentials", async () => {
test.skip("Cmd 'request.get' should return OK with HTTP 'proxy' param with credentials", async () => {
/*
To configure TinyProxy in local:
* sudo vim /etc/tinyproxy/tinyproxy.conf
@@ -279,6 +340,32 @@ describe("Test '/v1' path", () => {
expect(solution.status).toContain(200)
});
test("Cmd 'request.get' should return OK with SOCKSv5 'proxy' param", async () => {
/*
To configure Dante in local:
* https://linuxhint.com/set-up-a-socks5-proxy-on-ubuntu-with-dante/
* sudo vim /etc/sockd.conf
* sudo systemctl restart sockd.service
* curl --socks5 socks5://127.0.0.1:1080 https://www.google.com
*/
const payload = {
"cmd": "request.get",
"url": googleUrl,
"proxy": {
"url": proxySocksUrl
}
}
const response: Response = await request(app).post("/v1").send(payload);
expect(response.statusCode).toBe(200);
const apiResponse: V1ResponseSolution = response.body;
expect(apiResponse.status).toBe("ok");
const solution = apiResponse.solution;
expect(solution.url).toContain(googleUrl)
expect(solution.status).toBe(200);
});
test("Cmd 'request.get' should fail with wrong 'proxy' param", async () => {
const payload = {
"cmd": "request.get",