Compare commits

...

14 Commits
v1.2.6 ... v1

Author SHA1 Message Date
ngosang
1b01caaa78 Bump version 1.2.9 2021-08-01 22:11:55 +02:00
ngosang
447c8f67a1 Improve "Execution context was destroyed" error handling 2021-08-01 22:10:53 +02:00
ngosang
9dae74bc28 Implement returnRawHtml parameter. resolves #172 resolves #165 2021-08-01 22:08:55 +02:00
ngosang
4199db5a41 Capture Docker stop signal. resolves #158 2021-08-01 21:37:45 +02:00
ngosang
2a4fae37c0 Reduce Docker image size 20 MB 2021-08-01 21:27:27 +02:00
ngosang
232ddca512 Fix page reload after challenge is solved. resolves #162 resolves #143 2021-08-01 20:34:38 +02:00
ngosang
8572fab781 Avoid loading images/css/fonts to speed up page load 2021-08-01 19:35:26 +02:00
ngosang
fdb3eae051 Improve Cloudflare IP ban detection 2021-08-01 19:32:09 +02:00
ngosang
6dd8206a10 Fix vulnerabilities 2021-08-01 19:15:24 +02:00
ngosang
c4e4d28c8d Bump version 1.2.8 2021-06-01 02:00:39 +02:00
ngosang
543ce89eb6 Improve old JS challenge waiting. Resolves #129 2021-06-01 01:59:57 +02:00
ngosang
0f30e17ef1 Bump version 1.2.7 2021-06-01 01:22:36 +02:00
ngosang
24f1b4ec6f Improvements in Cloudflare redirect detection. Resolves #140 2021-06-01 01:21:06 +02:00
ngosang
f3b30268c3 Fix installation instructions 2021-05-31 22:59:51 +02:00
7 changed files with 86 additions and 47 deletions

View File

@@ -22,8 +22,8 @@ ENV PUPPETEER_PRODUCT=chrome \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
RUN npm install && \ RUN npm install && \
npm run build && \ npm run build && \
rm -rf src tsconfig.json && \ npm prune --production && \
npm prune --production rm -rf /home/node/.npm
EXPOSE 8191 EXPOSE 8191
ENTRYPOINT ["/usr/bin/dumb-init", "--"] ENTRYPOINT ["/usr/bin/dumb-init", "--"]

View File

@@ -8,9 +8,7 @@
[![Donate Buy Me A Coffee](https://img.shields.io/badge/Donate-Buy%20me%20a%20coffee-yellow.svg)](https://www.buymeacoffee.com/ngosang) [![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://img.shields.io/badge/Donate-Bitcoin-orange.svg)](https://en.cryptobadges.io/donate/13Hcv77AdnFWEUZ9qUpoPBttQsUT7q9TTh)
FlareSolverr is a proxy server to bypass Cloudflare protection FlareSolverr is a proxy server to bypass Cloudflare protection.
:warning: This project is in beta state. Some things may not work and the API can change at any time.
## How it works ## How it works
@@ -68,11 +66,12 @@ 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/).
* Clone this repository and open a shell in that path * Clone this repository and open a shell in that path.
* Run `npm install` command to install FlareSolverr dependencies * Run `npm install` command to install FlareSolverr dependencies.
* Run `npm run build` command to compile TypeScript code * Run `node node_modules/puppeteer/install.js` to install Chromium.
* Run `npm start` command to start FlareSolverr * Run `npm run build` command to compile TypeScript code.
* Run `npm start` command to start FlareSolverr.
### Systemd service ### Systemd service
@@ -147,6 +146,7 @@ headers | Optional. To specify user headers.
maxTimeout | Optional, default value 60000. Max timeout to solve the challenge in milliseconds. 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. 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. returnOnlyCookies | Optional, default false. Only returns the cookies. Response data, headers and other parts of the response are removed.
returnRawHtml | Optional, default false. The response data will be returned without JS processing. This is useful for JSON or plain text content.
Example response from running the `curl` above: Example response from running the `curl` above:

40
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "flaresolverr", "name": "flaresolverr",
"version": "1.2.6", "version": "1.2.9",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "flaresolverr", "name": "flaresolverr",
"version": "1.2.5", "version": "1.2.8",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"await-timeout": "^1.1.1", "await-timeout": "^1.1.1",
@@ -2096,9 +2096,9 @@
} }
}, },
"node_modules/glob-parent": { "node_modules/glob-parent": {
"version": "5.1.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"is-glob": "^4.0.1" "is-glob": "^4.0.1"
@@ -2913,9 +2913,9 @@
"dev": true "dev": true
}, },
"node_modules/merge-deep": { "node_modules/merge-deep": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz", "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz",
"integrity": "sha512-T7qC8kg4Zoti1cFd8Cr0M+qaZfOwjlPDEdZIIPPB2JZctjaPM4fX+i7HOId69tAti2fvO6X5ldfYUONDODsrkA==", "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==",
"dependencies": { "dependencies": {
"arr-union": "^3.1.0", "arr-union": "^3.1.0",
"clone-deep": "^0.2.4", "clone-deep": "^0.2.4",
@@ -3178,9 +3178,9 @@
} }
}, },
"node_modules/normalize-url": { "node_modules/normalize-url": {
"version": "4.5.0", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
"integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -6860,9 +6860,9 @@
} }
}, },
"glob-parent": { "glob-parent": {
"version": "5.1.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true, "dev": true,
"requires": { "requires": {
"is-glob": "^4.0.1" "is-glob": "^4.0.1"
@@ -7523,9 +7523,9 @@
"dev": true "dev": true
}, },
"merge-deep": { "merge-deep": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz", "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz",
"integrity": "sha512-T7qC8kg4Zoti1cFd8Cr0M+qaZfOwjlPDEdZIIPPB2JZctjaPM4fX+i7HOId69tAti2fvO6X5ldfYUONDODsrkA==", "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==",
"requires": { "requires": {
"arr-union": "^3.1.0", "arr-union": "^3.1.0",
"clone-deep": "^0.2.4", "clone-deep": "^0.2.4",
@@ -7739,9 +7739,9 @@
"dev": true "dev": true
}, },
"normalize-url": { "normalize-url": {
"version": "4.5.0", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
"integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA=="
}, },
"oauth-sign": { "oauth-sign": {
"version": "0.9.0", "version": "0.9.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "flaresolverr", "name": "flaresolverr",
"version": "1.2.6", "version": "1.2.9",
"description": "Proxy server to bypass Cloudflare protection.", "description": "Proxy server to bypass Cloudflare protection.",
"scripts": { "scripts": {
"start": "node ./dist/index.js", "start": "node ./dist/index.js",

View File

@@ -1,6 +1,7 @@
const fs = require('fs'); const fs = require('fs');
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const process = require('process')
import log from './log' import log from './log'
import { createServer, IncomingMessage, ServerResponse } from 'http'; import { createServer, IncomingMessage, ServerResponse } from 'http';
import { RequestContext } from './types' import { RequestContext } from './types'
@@ -118,7 +119,15 @@ function validateIncomingRequest(ctx: RequestContext, params: BaseAPICall) {
// init // init
log.info(`FlareSolverr ${version}`); log.info(`FlareSolverr ${version}`);
log.debug('Debug log enabled'); log.debug('Debug log enabled');
process.on('SIGTERM', () => {
// Capture signal on Docker Stop #158
log.info("Process interrupted")
process.exit(0)
})
validateEnvironmentVariables(); validateEnvironmentVariables();
testChromeInstallation() testChromeInstallation()
.catch(e => { .catch(e => {
log.error("Error starting Chrome browser.", e); log.error("Error starting Chrome browser.", e);

View File

@@ -20,8 +20,8 @@ export default async function resolveChallenge(url: string, page: Page, response
} }
log.info('Cloudflare detected'); log.info('Cloudflare detected');
if (await page.$('.cf-error-code')) { if (await page.$('span[data-translate="error"]') || (await page.content()).includes('error code: 1020')) {
throw new Error('Cloudflare has blocked this request (Code 1020 Detected).') throw new Error('Cloudflare has blocked this request. Probably your IP is banned for this site, check in your web browser.')
} }
let selectorFoundCount = 0; let selectorFoundCount = 0;
@@ -35,37 +35,51 @@ export default async function resolveChallenge(url: string, page: Page, response
log.debug('Waiting for Cloudflare challenge...') log.debug('Waiting for Cloudflare challenge...')
while (true) { while (true) {
await page.waitFor(1000)
try {
// catch exception timeout in waitForNavigation
response = await page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 9000 })
} catch (error) { }
try { try {
// catch Execution context was destroyed // catch Execution context was destroyed
const cfChallengeElem = await page.$(selector) const cfChallengeElem = await page.$(selector)
if (!cfChallengeElem) { if (!cfChallengeElem) {
// solved! // solved!
log.debug('Challenge element not found.')
break break
} else { } else {
// new Cloudflare Challenge #cf-please-wait
const displayStyle = await page.evaluate((selector) => { const displayStyle = await page.evaluate((selector) => {
return getComputedStyle(document.querySelector(selector)).getPropertyValue("display"); return getComputedStyle(document.querySelector(selector)).getPropertyValue("display");
}, selector); }, selector);
if (displayStyle == "none") { if (displayStyle == "none") {
// spinner is hidden, could be a captcha or not // spinner is hidden, could be a captcha or not
await page.waitFor(1000) 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 break
} else {
log.debug('Challenge element is visible.')
} }
} }
log.debug('Found challenge element again...') log.debug('Found challenge element again.')
} catch (error) } catch (error)
{ {
log.debug("Unexpected error: " + error); log.debug("Unexpected error: " + error);
if (!error.toString().includes("Execution context was destroyed")) {
break
}
} }
response = await page.reload({ waitUntil: 'domcontentloaded' }) log.debug('Waiting for Cloudflare challenge...')
log.debug('Page reloaded.') await page.waitFor(1000)
log.html(await page.content())
} }
log.debug('Validating HTML code...') log.debug('Validating HTML code...')
@@ -157,7 +171,7 @@ export default async function resolveChallenge(url: string, page: Page, response
} }
// submit captcha response // submit captcha response
challengeForm.evaluate((e: HTMLFormElement) => e.submit()) await challengeForm.evaluate((e: HTMLFormElement) => e.submit())
response = await page.waitForNavigation({ waitUntil: 'domcontentloaded' }) response = await page.waitForNavigation({ waitUntil: 'domcontentloaded' })
if (await page.$('input[name="cf_captcha_kind"]')) { if (await page.$('input[name="cf_captcha_kind"]')) {
@@ -171,6 +185,12 @@ export default async function resolveChallenge(url: string, page: Page, response
if (selectorFoundCount == 0) if (selectorFoundCount == 0)
{ {
throw new Error('No challenge selectors found, unable to proceed') throw new Error('No challenge selectors found, unable to proceed')
} else {
// reload the page to make sure we get the real response
// do not use page.reload() to avoid #162 #143
response = await page.goto(url, { waitUntil: 'domcontentloaded' })
await page.content()
log.info('Challenge solved.');
} }
} }

View File

@@ -36,9 +36,9 @@ interface BaseRequestAPICall extends BaseAPICall {
proxy?: any, // TODO: use interface not any proxy?: any, // TODO: use interface not any
download?: boolean download?: boolean
returnOnlyCookies?: boolean returnOnlyCookies?: boolean
returnRawHtml?: boolean
} }
interface Routes { interface Routes {
[key: string]: (ctx: RequestContext, params: BaseAPICall) => void | Promise<void> [key: string]: (ctx: RequestContext, params: BaseAPICall) => void | Promise<void>
} }
@@ -86,7 +86,9 @@ async function resolveChallengeWithTimeout(ctx: RequestContext, params: BaseRequ
} }
} }
async function resolveChallenge(ctx: RequestContext, { url, proxy, download, returnOnlyCookies }: BaseRequestAPICall, page: Page): Promise<ChallengeResolutionT | void> { async function resolveChallenge(ctx: RequestContext,
{ url, proxy, download, returnOnlyCookies, returnRawHtml }: BaseRequestAPICall,
page: Page): Promise<ChallengeResolutionT | void> {
let status = 'ok' let status = 'ok'
let message = '' let message = ''
@@ -132,6 +134,8 @@ async function resolveChallenge(ctx: RequestContext, { url, proxy, download, ret
// fix since I am short on time // fix since I am short on time
response = await page.goto(url, { waitUntil: 'domcontentloaded' }) response = await page.goto(url, { waitUntil: 'domcontentloaded' })
payload.result.response = (await response.buffer()).toString('base64') payload.result.response = (await response.buffer()).toString('base64')
} else if (returnRawHtml) {
payload.result.response = await response.text()
} else { } else {
payload.result.response = await page.content() payload.result.response = await page.content()
} }
@@ -197,6 +201,12 @@ async function setupPage(ctx: RequestContext, params: BaseRequestAPICall, browse
let callbackRunOnce = false let callbackRunOnce = false
const callback = (request: Request) => { const callback = (request: Request) => {
// avoid loading resources to speed up page load
if(request.resourceType() == 'stylesheet' || request.resourceType() == 'font' || request.resourceType() == 'image') {
request.abort()
return
}
if (callbackRunOnce || !request.isNavigationRequest()) { if (callbackRunOnce || !request.isNavigationRequest()) {
request.continue() request.continue()
return return