Compare commits

...

13 Commits

Author SHA1 Message Date
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
11 changed files with 2917 additions and 4063 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

@@ -66,13 +66,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
@@ -221,7 +221,7 @@ 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();

6700
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
{
"name": "flaresolverr",
"version": "2.1.0",
"version": "2.2.1",
"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)",
@@ -23,7 +23,7 @@
"body-parser": "^1.19.0",
"console-log-level": "^1.4.1",
"express": "^4.17.1",
"puppeteer": "^3.3.0",
"puppeteer": "^13.1.2",
"uuid": "^8.3.2"
},
"devDependencies": {
@@ -31,13 +31,12 @@
"@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/node": "^16.11.7",
"@types/supertest": "^2.0.11",
"@types/uuid": "^8.3.1",
"archiver": "^5.3.0",
"nodemon": "^2.0.13",
"pkg": "^5.3.3",
"pkg": "^5.5.2",
"supertest": "^6.1.6",
"ts-jest": "^27.0.7",
"ts-node": "^10.3.0",

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";
@@ -7,15 +7,29 @@ import log from "../services/log";
**/
const BAN_SELECTORS = ['.text-gray-600'];
const CHALLENGE_SELECTORS = ['#trk_jschal_js', '.ray_id', '.attack-box', '#cf-please-wait'];
const CHALLENGE_SELECTORS = [
'#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 = ['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');
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');
@@ -26,76 +40,68 @@ export default async function resolveChallenge(url: string, page: Page, response
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
}
@@ -56,8 +57,12 @@ 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 = {
"network.proxy.type": 1,
@@ -117,7 +122,8 @@ export async function testWebBrowserInstallation(): Promise<void> {
oneTimeSession: true
})
const page = await session.browser.newPage()
await page.goto(testUrl)
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
@@ -133,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
@@ -140,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: 2000});
}
} 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

@@ -15,6 +15,8 @@ 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://www.erai-raws.info/feed/?type=magnet";
const ccfUrl = "https://www.muziekfabriek.org";
beforeAll(async () => {
// Init session
@@ -168,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("<rss version")
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",