Compare commits

..

8 Commits

Author SHA1 Message Date
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
10 changed files with 2899 additions and 4044 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 # Print build information
ARG TARGETPLATFORM ARG TARGETPLATFORM

View File

@@ -66,13 +66,13 @@ This is the recommended way for Windows users.
### From source code ### From source code
This is the recommended way for macOS users and for developers. 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. * Clone this repository and open a shell in that path.
* Run `export PUPPETEER_PRODUCT=firefox` (Linux/macOS) or `set PUPPETEER_PRODUCT=firefox` (Windows). * Run `export PUPPETEER_PRODUCT=firefox` (Linux/macOS) or `set PUPPETEER_PRODUCT=firefox` (Windows).
* Run `npm install` command to install FlareSolverr dependencies. * Run `npm install` command to install FlareSolverr dependencies.
* Run `node node_modules/puppeteer/install.js` to install Firefox. * Run `npm start` command to compile TypeScript code and start FlareSolverr.
* Run `npm run build` command to compile TypeScript code.
* Run `npm start` command to start FlareSolverr. If you get errors related to firefox not installed try running `node node_modules/puppeteer/install.js` to install Firefox.
### Systemd service ### 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. 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`. 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. 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. 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. 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. 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')) { if (fs.existsSync('bin')) {
fs.rmSync('bin', { recursive: true }) 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 node16-win-x64,node16-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-mac-x64,node16-linux-x64 --out-path bin .')
// get firefox revision // get firefox revision
const revision = await getFirefoxNightlyVersion(); const revision = await getFirefoxNightlyVersion();

6673
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
{ {
"name": "flaresolverr", "name": "flaresolverr",
"version": "2.1.0", "version": "2.2.0",
"description": "Proxy server to bypass Cloudflare protection.", "description": "Proxy server to bypass Cloudflare protection.",
"scripts": { "scripts": {
"start": "node ./dist/server.js", "start": "tsc && node ./dist/server.js",
"build": "tsc", "build": "tsc",
"dev": "nodemon -e ts --exec ts-node src/server.ts", "dev": "nodemon -e ts --exec ts-node src/server.ts",
"package": "node build-binaries.js", "package": "tsc && node build-binaries.js",
"test": "jest --runInBand" "test": "jest --runInBand"
}, },
"author": "Diego Heras (ngosang)", "author": "Diego Heras (ngosang)",
@@ -23,7 +23,7 @@
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"console-log-level": "^1.4.1", "console-log-level": "^1.4.1",
"express": "^4.17.1", "express": "^4.17.1",
"puppeteer": "^3.3.0", "puppeteer": "^13.1.2",
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
@@ -31,13 +31,13 @@
"@types/body-parser": "^1.19.1", "@types/body-parser": "^1.19.1",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jest": "^27.0.2", "@types/jest": "^27.0.2",
"@types/node": "^14.17.27", "@types/node": "^16.11.7",
"@types/puppeteer": "^3.0.6", "@types/puppeteer": "^5.4.4",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.11",
"@types/uuid": "^8.3.1", "@types/uuid": "^8.3.1",
"archiver": "^5.3.0", "archiver": "^5.3.0",
"nodemon": "^2.0.13", "nodemon": "^2.0.13",
"pkg": "^5.3.3", "pkg": "^5.5.2",
"supertest": "^6.1.6", "supertest": "^6.1.6",
"ts-jest": "^27.0.7", "ts-jest": "^27.0.7",
"ts-node": "^10.3.0", "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 {Request, Response} from 'express';
import {Protocol} from "devtools-protocol";
import log from '../services/log' import log from '../services/log'
import {browserRequest, ChallengeResolutionResultT, ChallengeResolutionT} from "../services/solver"; import {browserRequest, ChallengeResolutionResultT, ChallengeResolutionT} from "../services/solver";
@@ -20,11 +19,11 @@ export interface Proxy {
export interface V1RequestBase { export interface V1RequestBase {
cmd: string cmd: string
cookies?: SetCookie[], cookies?: Protocol.Network.CookieParam[],
maxTimeout?: number maxTimeout?: number
proxy?: Proxy proxy?: Proxy
session: string session: string
headers?: Headers // deprecated v2, not used headers?: Record<string, string> // deprecated v2, not used
userAgent?: string // deprecated v2, not used userAgent?: string // deprecated v2, not used
} }
@@ -33,7 +32,7 @@ interface V1RequestSession extends V1RequestBase {
export interface V1Request extends V1RequestBase { export interface V1Request extends V1RequestBase {
url: string url: string
method?: HttpMethod method?: string
postData?: string postData?: string
returnOnlyCookies?: boolean returnOnlyCookies?: boolean
download?: boolean // deprecated v2, not used 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"; import log from "../services/log";
@@ -7,15 +7,29 @@ import log from "../services/log";
**/ **/
const BAN_SELECTORS = ['.text-gray-600']; 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"]']; 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 // look for challenge and return fast if not detected
if (response.headers().server && let cfDetected = response.headers().server && response.headers().server.startsWith('cloudflare');
response.headers().server.startsWith('cloudflare') && if (cfDetected) {
(response.status() == 403 || response.status() == 503)) { 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'); log.info('Cloudflare detected');
} else { } else {
log.info('Cloudflare not detected'); log.info('Cloudflare not detected');
@@ -26,10 +40,8 @@ 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.') throw new Error('Cloudflare has blocked this request. Probably your IP is banned for this site, check in your web browser.')
} }
let selectorFound = false;
if (response.status() > 400) {
// find Cloudflare selectors // find Cloudflare selectors
let selectorFound = false;
let selector: string = await findAnySelector(page, CHALLENGE_SELECTORS) let selector: string = await findAnySelector(page, CHALLENGE_SELECTORS)
if (selector) { if (selector) {
selectorFound = true; selectorFound = true;
@@ -57,7 +69,7 @@ export default async function resolveChallenge(url: string, page: Page, response
// wait until redirecting disappears // wait until redirecting disappears
while (true) { while (true) {
try { try {
await page.waitFor(1000) await page.waitForTimeout(1000)
const displayStyle2 = await page.evaluate(() => { const displayStyle2 = await page.evaluate(() => {
return getComputedStyle(document.querySelector('#cf-spinner-redirecting')).getPropertyValue("display"); return getComputedStyle(document.querySelector('#cf-spinner-redirecting')).getPropertyValue("display");
}); });
@@ -84,7 +96,7 @@ export default async function resolveChallenge(url: string, page: Page, response
} }
log.debug('Waiting for Cloudflare challenge...') log.debug('Waiting for Cloudflare challenge...')
await page.waitFor(1000) await page.waitForTimeout(1000)
} }
log.debug('Validating HTML code...') log.debug('Validating HTML code...')
@@ -92,12 +104,6 @@ export default async function resolveChallenge(url: string, page: Page, response
log.debug(`No challenge element detected.`) log.debug(`No challenge element detected.`)
} }
} else {
// some sites use cloudflare but there is no challenge
log.debug(`Javascript challenge not detected. Status code: ${response.status()}`);
selectorFound = true;
}
// check for CAPTCHA challenge // check for CAPTCHA challenge
if (await findAnySelector(page, CAPTCHA_SELECTORS)) { if (await findAnySelector(page, CAPTCHA_SELECTORS)) {
log.info('CAPTCHA challenge detected'); log.info('CAPTCHA challenge detected');

View File

@@ -1,6 +1,7 @@
import {v1 as UUIDv1} from 'uuid' import {v1 as UUIDv1} from 'uuid'
import * as path from 'path' import * as path from 'path'
import {SetCookie, Browser} from 'puppeteer' import {Browser} from 'puppeteer'
import {Protocol} from "devtools-protocol";
import log from './log' import log from './log'
import {Proxy} from "../controllers/v1"; import {Proxy} from "../controllers/v1";
@@ -20,7 +21,7 @@ interface SessionsCache {
export interface SessionCreateOptions { export interface SessionCreateOptions {
oneTimeSession: boolean oneTimeSession: boolean
cookies?: SetCookie[], cookies?: Protocol.Network.CookieParam[],
maxTimeout?: number maxTimeout?: number
proxy?: Proxy proxy?: Proxy
} }
@@ -140,7 +141,7 @@ export async function create(session: string, options: SessionCreateOptions): Pr
const puppeteerOptions: any = { const puppeteerOptions: any = {
product: 'firefox', product: 'firefox',
headless: process.env.HEADLESS !== 'false', headless: process.env.HEADLESS !== 'false',
timeout: process.env.BROWSER_TIMEOUT || 30000 timeout: process.env.BROWSER_TIMEOUT || 40000
} }
puppeteerOptions.extraPrefsFirefox = buildExtraPrefsFirefox(options.proxy) 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'); const Timeout = require('await-timeout');
import log from './log' import log from './log'
@@ -11,7 +11,7 @@ const sessions = require('./sessions')
export interface ChallengeResolutionResultT { export interface ChallengeResolutionResultT {
url: string url: string
status: number, status: number,
headers?: Headers, headers?: Record<string, string>,
response: string, response: string,
cookies: object[] cookies: object[]
userAgent: string userAgent: string
@@ -64,7 +64,7 @@ async function resolveChallenge(params: V1Request, session: SessionsCacheItem):
// go to the page // go to the page
log.debug(`Navigating to... ${params.url}`) log.debug(`Navigating to... ${params.url}`)
let response: Response = await gotoPage(params, page); let response: HTTPResponse = await gotoPage(params, page);
// set cookies // set cookies
if (params.cookies) { if (params.cookies) {
@@ -128,8 +128,8 @@ async function resolveChallenge(params: V1Request, session: SessionsCacheItem):
} }
} }
async function gotoPage(params: V1Request, page: Page): Promise<Response> { async function gotoPage(params: V1Request, page: Page): Promise<HTTPResponse> {
let response: Response; let response: HTTPResponse;
if (params.method != 'POST') { if (params.method != 'POST') {
response = await page.goto(params.url, {waitUntil: 'domcontentloaded'}); response = await page.goto(params.url, {waitUntil: 'domcontentloaded'});
@@ -177,7 +177,7 @@ async function gotoPage(params: V1Request, page: Page): Promise<Response> {
</html> </html>
` `
); );
await page.waitFor(2000) await page.waitForTimeout(2000)
try { try {
await page.waitForNavigation({waitUntil: 'domcontentloaded', timeout: 2000}) await page.waitForNavigation({waitUntil: 'domcontentloaded', timeout: 2000})
} catch (e) {} } 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 cfUrl = "https://pirateiro.com/torrents/?search=harry";
const cfCaptchaUrl = "https://idope.se" const cfCaptchaUrl = "https://idope.se"
const cfBlockedUrl = "https://www.torrentmafya.org/table.php" 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 () => { beforeAll(async () => {
// Init session // Init session
@@ -168,6 +170,64 @@ describe("Test '/v1' path", () => {
expect(apiResponse.solution.url).toContain(cfBlockedUrl) 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 () => { test("Cmd 'request.get' should return OK with 'cookies' param", async () => {
const payload = { const payload = {
"cmd": "request.get", "cmd": "request.get",