mirror of
https://github.com/FlareSolverr/FlareSolverr.git
synced 2025-12-05 17:18:19 +01:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f30e17ef1 | ||
|
|
24f1b4ec6f | ||
|
|
f3b30268c3 | ||
|
|
be4354c68d | ||
|
|
5242cf3359 | ||
|
|
c6677f4d84 | ||
|
|
805a34c9d6 | ||
|
|
2f9fe05a76 | ||
|
|
8961d67a29 | ||
|
|
5da5156851 | ||
|
|
05f8ef95d9 | ||
|
|
10f8b83e83 | ||
|
|
6cf948d0e1 | ||
|
|
dcdc70273f | ||
|
|
e2dc39ee4e | ||
|
|
340638ca54 | ||
|
|
05abe69df6 | ||
|
|
e596906c19 |
1
.github/ISSUE_TEMPLATE.md
vendored
1
.github/ISSUE_TEMPLATE.md
vendored
@@ -14,6 +14,7 @@ Check closed issues as well, because your issue may have already been fixed.
|
|||||||
* **Are you using a proxy or VPN?** [yes/no]
|
* **Are you using a proxy or VPN?** [yes/no]
|
||||||
* **Are you using Captcha Solver:** [yes/no]
|
* **Are you using Captcha Solver:** [yes/no]
|
||||||
* **If using captcha solver, which one:**
|
* **If using captcha solver, which one:**
|
||||||
|
* **URL to test this issue:**
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/release-docker.yml
vendored
2
.github/workflows/release-docker.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
tag-sha: false
|
tag-sha: false
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1.0.1
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -8,9 +8,7 @@
|
|||||||
[](https://www.buymeacoffee.com/ngosang)
|
[](https://www.buymeacoffee.com/ngosang)
|
||||||
[](https://en.cryptobadges.io/donate/13Hcv77AdnFWEUZ9qUpoPBttQsUT7q9TTh)
|
[](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
|
||||||
|
|
||||||
@@ -67,12 +65,17 @@ 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
|
||||||
|
|
||||||
|
We provide an example Systemd unit file `flaresolverr.service` as reference. You have to modify the file to suit your needs: paths, user and environment variables.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -226,7 +229,8 @@ Name | Default | Notes
|
|||||||
|--|--|--|
|
|--|--|--|
|
||||||
LOG_LEVEL | info | Verbosity of the logging. Use `LOG_LEVEL=debug` for more information.
|
LOG_LEVEL | info | Verbosity of the logging. Use `LOG_LEVEL=debug` for more information.
|
||||||
LOG_HTML | false | Only for debugging. If `true` all HTML that passes through the proxy will be logged to the console in `debug` level.
|
LOG_HTML | false | Only for debugging. If `true` all HTML that passes through the proxy will be logged to the console in `debug` level.
|
||||||
CAPTCHA_SOLVER | none | Captcha solving method. It 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`.
|
||||||
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.
|
||||||
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.
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const version = 'v' + require('./package.json').version;
|
|||||||
// generate executables
|
// generate executables
|
||||||
console.log('Generating executables...')
|
console.log('Generating executables...')
|
||||||
if (fs.existsSync('bin')) {
|
if (fs.existsSync('bin')) {
|
||||||
fs.rmdirSync('bin', { recursive: true })
|
fs.rmSync('bin', { recursive: true })
|
||||||
}
|
}
|
||||||
execSync('pkg -t node14-win-x64,node14-linux-x64 --out-path bin .')
|
execSync('pkg -t node14-win-x64,node14-linux-x64 --out-path bin .')
|
||||||
// execSync('pkg -t node14-win-x64,node14-mac-x64,node14-linux-x64 --out-path bin .')
|
// execSync('pkg -t node14-win-x64,node14-mac-x64,node14-linux-x64 --out-path bin .')
|
||||||
@@ -76,6 +76,9 @@ const version = 'v' + require('./package.json').version;
|
|||||||
archive.file('LICENSE', { name: 'flaresolverr/' + os.fsLicenseName })
|
archive.file('LICENSE', { name: 'flaresolverr/' + os.fsLicenseName })
|
||||||
archive.file('bin/' + os.fsExec, { name: 'flaresolverr/' + os.fsZipExec })
|
archive.file('bin/' + os.fsExec, { name: 'flaresolverr/' + os.fsZipExec })
|
||||||
archive.directory('bin/puppeteer/' + os.platform + '-' + os.version + '/' + os.chromeFolder, 'flaresolverr/chrome')
|
archive.directory('bin/puppeteer/' + os.platform + '-' + os.version + '/' + os.chromeFolder, 'flaresolverr/chrome')
|
||||||
|
if (os.platform === 'linux') {
|
||||||
|
archive.file('flaresolverr.service', { name: 'flaresolverr/flaresolverr.service' })
|
||||||
|
}
|
||||||
|
|
||||||
await archive.finalize()
|
await archive.finalize()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ services:
|
|||||||
- LOG_LEVEL=${LOG_LEVEL:-info}
|
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
- LOG_HTML=${LOG_HTML:-false}
|
- LOG_HTML=${LOG_HTML:-false}
|
||||||
- CAPTCHA_SOLVER=${CAPTCHA_SOLVER:-none}
|
- CAPTCHA_SOLVER=${CAPTCHA_SOLVER:-none}
|
||||||
|
- TZ=Europe/London
|
||||||
ports:
|
ports:
|
||||||
- "${PORT:-8191}:8191"
|
- "${PORT:-8191}:8191"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
19
flaresolverr.service
Normal file
19
flaresolverr.service
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=FlareSolverr
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
SyslogIdentifier=flaresolverr
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Type=simple
|
||||||
|
User=flaresolverr
|
||||||
|
Group=flaresolverr
|
||||||
|
Environment="LOG_LEVEL=info"
|
||||||
|
Environment="CAPTCHA_SOLVER=none"
|
||||||
|
WorkingDirectory=/opt/flaresolverr
|
||||||
|
ExecStart=/opt/flaresolverr/flaresolverr
|
||||||
|
TimeoutStopSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
5196
package-lock.json
generated
5196
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flaresolverr",
|
"name": "flaresolverr",
|
||||||
"version": "1.2.4",
|
"version": "1.2.7",
|
||||||
"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",
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ import { SolverOptions } from '.'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export default async function solve({ url }: SolverOptions): Promise<string> {
|
export default async function solve({ url }: SolverOptions): Promise<string> {
|
||||||
|
throw new Error("hcaptcha-solver is not able to solve the new hCaptcha challenge. This issue is already reported #31.");
|
||||||
|
|
||||||
|
/*
|
||||||
try {
|
try {
|
||||||
return await solveCaptcha(url)
|
return await solveCaptcha(url)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
31
src/index.ts
31
src/index.ts
@@ -36,19 +36,27 @@ function validateEnvironmentVariables() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function testChromeInstallation() {
|
async function testChromeInstallation() {
|
||||||
log.debug("Testing Chrome installation...")
|
const sessionId = UUIDv1()
|
||||||
// create a temporary file for testing
|
// create a temporary file for testing
|
||||||
const filePath = path.join(os.tmpdir(), 'flaresolverr.txt')
|
log.debug("Testing Chrome installation...")
|
||||||
fs.writeFileSync(filePath, 'flaresolverr');
|
const fileContent = `flaresolverr_${version}`
|
||||||
|
const filePath = path.join(os.tmpdir(), `flaresolverr_${sessionId}.txt`)
|
||||||
|
const fileUrl = `file://${filePath}`
|
||||||
|
fs.writeFileSync(filePath, fileContent)
|
||||||
// launch the browser
|
// launch the browser
|
||||||
const url = `file://${filePath}`;
|
const session = await sessions.create(sessionId, {
|
||||||
const session = await sessions.create(UUIDv1(), {
|
|
||||||
userAgent: null,
|
userAgent: null,
|
||||||
oneTimeSession: true
|
oneTimeSession: true
|
||||||
})
|
})
|
||||||
const page = await session.browser.newPage()
|
const page = await session.browser.newPage()
|
||||||
await page.goto(url, { waitUntil: 'domcontentloaded' })
|
const response = await page.goto(fileUrl, { waitUntil: 'domcontentloaded' })
|
||||||
log.debug("Test successful.")
|
const responseBody = (await response.buffer()).toString().trim()
|
||||||
|
if (responseBody != fileContent) {
|
||||||
|
throw new Error("The response body does not match!")
|
||||||
|
}
|
||||||
|
await page.close()
|
||||||
|
await sessions.destroy(sessionId)
|
||||||
|
log.debug("Test successful")
|
||||||
}
|
}
|
||||||
|
|
||||||
function errorResponse(errorMsg: string, res: ServerResponse, startTimestamp: number) {
|
function errorResponse(errorMsg: string, res: ServerResponse, startTimestamp: number) {
|
||||||
@@ -69,7 +77,7 @@ function errorResponse(errorMsg: string, res: ServerResponse, startTimestamp: nu
|
|||||||
|
|
||||||
function successResponse(successMsg: string, extendedProperties: object, res: ServerResponse, startTimestamp: number) {
|
function successResponse(successMsg: string, extendedProperties: object, res: ServerResponse, startTimestamp: number) {
|
||||||
const endTimestamp = Date.now()
|
const endTimestamp = Date.now()
|
||||||
log.info(`Successful response in ${(endTimestamp - startTimestamp) / 1000} s`)
|
log.info(`Response in ${(endTimestamp - startTimestamp) / 1000} s`)
|
||||||
if (successMsg) { log.info(successMsg) }
|
if (successMsg) { log.info(successMsg) }
|
||||||
|
|
||||||
const response = Object.assign({
|
const response = Object.assign({
|
||||||
@@ -111,7 +119,12 @@ function validateIncomingRequest(ctx: RequestContext, params: BaseAPICall) {
|
|||||||
log.info(`FlareSolverr ${version}`);
|
log.info(`FlareSolverr ${version}`);
|
||||||
log.debug('Debug log enabled');
|
log.debug('Debug log enabled');
|
||||||
validateEnvironmentVariables();
|
validateEnvironmentVariables();
|
||||||
testChromeInstallation().then(r =>
|
testChromeInstallation()
|
||||||
|
.catch(e => {
|
||||||
|
log.error("Error starting Chrome browser.", e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.then(r =>
|
||||||
createServer((req: IncomingMessage, res: ServerResponse) => {
|
createServer((req: IncomingMessage, res: ServerResponse) => {
|
||||||
const startTimestamp = Date.now()
|
const startTimestamp = Date.now()
|
||||||
|
|
||||||
|
|||||||
21
src/log.ts
21
src/log.ts
@@ -2,6 +2,25 @@ let requests = 0
|
|||||||
|
|
||||||
const LOG_HTML: boolean = process.env.LOG_HTML == 'true';
|
const LOG_HTML: boolean = process.env.LOG_HTML == 'true';
|
||||||
|
|
||||||
|
function toIsoString(date: Date) {
|
||||||
|
// this function fixes Date.toISOString() adding timezone
|
||||||
|
let tzo = -date.getTimezoneOffset(),
|
||||||
|
dif = tzo >= 0 ? '+' : '-',
|
||||||
|
pad = function(num: number) {
|
||||||
|
let norm = Math.floor(Math.abs(num));
|
||||||
|
return (norm < 10 ? '0' : '') + norm;
|
||||||
|
};
|
||||||
|
|
||||||
|
return date.getFullYear() +
|
||||||
|
'-' + pad(date.getMonth() + 1) +
|
||||||
|
'-' + pad(date.getDate()) +
|
||||||
|
'T' + pad(date.getHours()) +
|
||||||
|
':' + pad(date.getMinutes()) +
|
||||||
|
':' + pad(date.getSeconds()) +
|
||||||
|
dif + pad(tzo / 60) +
|
||||||
|
':' + pad(tzo % 60);
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
incRequests: () => { requests++ },
|
incRequests: () => { requests++ },
|
||||||
html(html: string) {
|
html(html: string) {
|
||||||
@@ -12,7 +31,7 @@ export default {
|
|||||||
{level: process.env.LOG_LEVEL || 'info',
|
{level: process.env.LOG_LEVEL || 'info',
|
||||||
prefix(level: string) {
|
prefix(level: string) {
|
||||||
const req = (requests > 0) ? ` REQ-${requests}` : '';
|
const req = (requests > 0) ? ` REQ-${requests}` : '';
|
||||||
return `${new Date().toISOString()} ${level.toUpperCase()}${req}`
|
return `${toIsoString(new Date())} ${level.toUpperCase()}${req}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import getCaptchaSolver, {CaptchaType} from "../captcha";
|
|||||||
* This class contains the logic to solve protections provided by CloudFlare
|
* This class contains the logic to solve protections provided by CloudFlare
|
||||||
**/
|
**/
|
||||||
|
|
||||||
const CHALLENGE_SELECTORS = ['#trk_jschal_js', '.ray_id', '.attack-box'];
|
const CHALLENGE_SELECTORS = ['#trk_jschal_js', '.ray_id', '.attack-box', '#cf-please-wait'];
|
||||||
const TOKEN_INPUT_NAMES = ['g-recaptcha-response', 'h-captcha-response'];
|
const TOKEN_INPUT_NAMES = ['g-recaptcha-response', 'h-captcha-response'];
|
||||||
|
|
||||||
export default async function resolveChallenge(url: string, page: Page, response: Response): Promise<Response> {
|
export default async function resolveChallenge(url: string, page: Page, response: Response): Promise<Response> {
|
||||||
@@ -35,23 +35,49 @@ 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: 5000 })
|
|
||||||
} 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) { break }
|
if (!cfChallengeElem) {
|
||||||
log.debug('Found challenge element again...')
|
// solved!
|
||||||
|
log.debug('Challenge element not found.')
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// 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)
|
} catch (error)
|
||||||
{ }
|
{
|
||||||
|
log.debug("Unexpected error: " + error);
|
||||||
|
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,6 +183,10 @@ 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
|
||||||
|
response = await page.reload()
|
||||||
|
log.info('Challenge solved.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,9 @@ async function resolveChallenge(ctx: RequestContext, { url, proxy, download, ret
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add final url in result
|
||||||
|
payload.result.url = page.url();
|
||||||
|
|
||||||
// make sure the page is closed because if it isn't and error will be thrown
|
// make sure the page is closed because if it isn't and error will be thrown
|
||||||
// when a user uses a temporary session, the browser make be quit before
|
// when a user uses a temporary session, the browser make be quit before
|
||||||
// the page is properly closed.
|
// the page is properly closed.
|
||||||
|
|||||||
Reference in New Issue
Block a user