mirror of
https://github.com/FlareSolverr/FlareSolverr.git
synced 2025-12-05 17:18:19 +01:00
Compare commits
57 Commits
drission-p
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fe9958afe | ||
|
|
9f8c71131f | ||
|
|
2405c00521 | ||
|
|
ff65b7cc68 | ||
|
|
409e0844a7 | ||
|
|
368d5d4e05 | ||
|
|
c7505e3cbf | ||
|
|
5a27090abe | ||
|
|
e505ea4fe4 | ||
|
|
63b6fc53e3 | ||
|
|
8d72617219 | ||
|
|
8a8b9415c3 | ||
|
|
16722ef963 | ||
|
|
bbc24e9d86 | ||
|
|
7dfdfc5e33 | ||
|
|
136422c85c | ||
|
|
05a72f2709 | ||
|
|
da810830da | ||
|
|
d27f57c27c | ||
|
|
a916d93779 | ||
|
|
0d889cb0b2 | ||
|
|
d430404de8 | ||
|
|
d3b1ba6e88 | ||
|
|
75e5b190d6 | ||
|
|
cdc3db3c21 | ||
|
|
2dbb0442e0 | ||
|
|
6faab19533 | ||
|
|
af0a7af757 | ||
|
|
ff74b50b60 | ||
|
|
3e51ac1188 | ||
|
|
6627de4fa6 | ||
|
|
fe649255f2 | ||
|
|
3e338fce2e | ||
|
|
3dd3e7559d | ||
|
|
f21c1d51bc | ||
|
|
957347f73a | ||
|
|
c55080b0ec | ||
|
|
639bfca020 | ||
|
|
237694df76 | ||
|
|
6e5d6f1795 | ||
|
|
30804a86e5 | ||
|
|
e0bdaf7745 | ||
|
|
795365dbe4 | ||
|
|
ce5369dd41 | ||
|
|
600b09d498 | ||
|
|
d1f19405a1 | ||
|
|
82a1366d34 | ||
|
|
a2fe9e7776 | ||
|
|
6cc628df9e | ||
|
|
8b1851eeb1 | ||
|
|
54668a11e7 | ||
|
|
701d8fb4ff | ||
|
|
39a265ccb8 | ||
|
|
e32b247014 | ||
|
|
0d8fe8fe50 | ||
|
|
718da3a36f | ||
|
|
a798561338 |
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -29,6 +29,13 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I have read the Discussions
|
- label: I have read the Discussions
|
||||||
required: true
|
required: true
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Have you ACTUALLY checked all these?
|
||||||
|
description: Please do not waste our time and yours; these checks are there for a reason, it is not just so you can tick boxes for fun. If you type <b>YES</b> and it is clear you did not or have put in no effort, your issue will be closed and locked without comment. If you type <b>NO</b> but still open this issue, you will be permanently blocked for timewasting.
|
||||||
|
placeholder: YES or NO
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Environment
|
label: Environment
|
||||||
|
|||||||
11
.github/workflows/autotag.yml
vendored
11
.github/workflows/autotag.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: autotag
|
name: Autotag
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -9,11 +9,10 @@ jobs:
|
|||||||
tag-release:
|
tag-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Checkout repository
|
||||||
name: Checkout
|
uses: actions/checkout@v5
|
||||||
uses: actions/checkout@v3
|
|
||||||
-
|
- name: Auto Tag
|
||||||
name: Auto Tag
|
|
||||||
uses: Klemensas/action-autotag@stable
|
uses: Klemensas/action-autotag@stable
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: "${{ secrets.GH_PAT }}"
|
GITHUB_TOKEN: "${{ secrets.GH_PAT }}"
|
||||||
|
|||||||
12
.github/workflows/release-docker.yml
vendored
12
.github/workflows/release-docker.yml
vendored
@@ -1,9 +1,9 @@
|
|||||||
name: release-docker
|
name: Docker release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- "v*.*.*"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
@@ -15,10 +15,10 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
build-docker-images:
|
build-docker-images:
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Downcase repo
|
- name: Downcase repo
|
||||||
run: echo REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
run: echo REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
|
||||||
@@ -43,8 +43,8 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|||||||
86
.github/workflows/release.yml
vendored
86
.github/workflows/release.yml
vendored
@@ -1,95 +1,63 @@
|
|||||||
name: release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- "v*.*.*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-release:
|
create-release:
|
||||||
name: Create release
|
name: Create release
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # get all commits, branches and tags (required for the changelog)
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Build changelog
|
- name: Build changelog
|
||||||
id: github_changelog
|
id: github_changelog
|
||||||
run: |
|
run: |
|
||||||
changelog=$(git log $(git tag | tail -2 | head -1)..HEAD --no-merges --oneline)
|
changelog=$(git log $(git tag | tail -2 | head -1)..HEAD --no-merges --oneline)
|
||||||
changelog="${changelog//'%'/'%25'}"
|
echo "changelog<<EOF" >> $GITHUB_OUTPUT
|
||||||
changelog="${changelog//$'\n'/'%0A'}"
|
echo "$changelog" >> $GITHUB_OUTPUT
|
||||||
changelog="${changelog//$'\r'/'%0D'}"
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
echo "##[set-output name=changelog;]${changelog}"
|
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
id: create_release
|
uses: softprops/action-gh-release@v2
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref }}
|
|
||||||
release_name: ${{ github.ref }}
|
|
||||||
body: ${{ steps.github_changelog.outputs.changelog }}
|
body: ${{ steps.github_changelog.outputs.changelog }}
|
||||||
draft: false
|
env:
|
||||||
prerelease: false
|
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||||
|
|
||||||
build-linux-package:
|
build-package:
|
||||||
name: Build Linux binary
|
name: Build binaries
|
||||||
needs: create-release
|
needs: create-release
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # get all commits, branches and tags (required for the changelog)
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: "3.13"
|
||||||
|
|
||||||
- name: Build artifacts
|
- name: Build artifacts
|
||||||
run: |
|
run: |
|
||||||
python -m pip install -r requirements.txt
|
python -m pip install -r requirements.txt
|
||||||
python -m pip install pyinstaller==5.13.0
|
python -m pip install pyinstaller==6.16.0
|
||||||
cd src
|
cd src
|
||||||
python build_package.py
|
python build_package.py
|
||||||
|
|
||||||
- name: Upload release artifacts
|
- name: Upload release artifacts
|
||||||
uses: alexellis/upload-assets@0.4.0
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: ./dist/flaresolverr_*
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
||||||
with:
|
|
||||||
asset_paths: '["./dist/flaresolverr_*"]'
|
|
||||||
|
|
||||||
build-windows-package:
|
|
||||||
name: Build Windows binary
|
|
||||||
needs: create-release
|
|
||||||
runs-on: windows-2022
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # get all commits, branches and tags (required for the changelog)
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Build artifacts
|
|
||||||
run: |
|
|
||||||
python -m pip install -r requirements.txt
|
|
||||||
python -m pip install pyinstaller==5.13.0
|
|
||||||
cd src
|
|
||||||
python build_package.py
|
|
||||||
|
|
||||||
- name: Upload release artifacts
|
|
||||||
uses: alexellis/upload-assets@0.4.0
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
|
|
||||||
with:
|
|
||||||
asset_paths: '["./dist/flaresolverr_*"]'
|
|
||||||
|
|||||||
48
CHANGELOG.md
48
CHANGELOG.md
@@ -1,5 +1,53 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v3.4.6 (2025/11/29)
|
||||||
|
* Add disable image, css, fonts option with CDP. Thanks @Ananto30
|
||||||
|
|
||||||
|
## v3.4.5 (2025/11/11)
|
||||||
|
* Revert to Python v3.13
|
||||||
|
|
||||||
|
## v3.4.4 (2025/11/04)
|
||||||
|
* Bump dependencies, Chromium, and some other general fixes. Thanks @flowerey
|
||||||
|
|
||||||
|
## v3.4.3 (2025/10/28)
|
||||||
|
* Update proxy extension
|
||||||
|
|
||||||
|
## v3.4.2 (2025/10/09)
|
||||||
|
* Bump dependencies & CI actions. Thanks @flowerey
|
||||||
|
* Add optional wait time after resolving the challenge before returning. Thanks @kennedyoliveira
|
||||||
|
* Add proxy ENVs. Thanks @Robokishan
|
||||||
|
* Handle empty string and keys without value in postData. Thanks @eZ4RK0
|
||||||
|
* Add quote protection for password containing it. Thanks @warrenberberd
|
||||||
|
* Add returnScreenshot parameter to screenshot the final web page. Thanks @estebanthi
|
||||||
|
* Add log file support. Thanks @acg5159
|
||||||
|
|
||||||
|
## v3.4.1 (2025/09/15)
|
||||||
|
* Fix regex pattern syntax in utils.py
|
||||||
|
* Change access denied title check to use startswith
|
||||||
|
|
||||||
|
## v3.4.0 (2025/08/25)
|
||||||
|
* Modernize and upgrade application. Thanks @TheCrazyLex
|
||||||
|
* Remove disable software rasterizer option for ARM builds. Thanks @smrodman83
|
||||||
|
|
||||||
|
## v3.3.25 (2025/06/14)
|
||||||
|
* Remove `use-gl` argument. Thanks @qwerty12
|
||||||
|
* u_c: remove apparent c&p typo. Thanks @ok3721
|
||||||
|
* Bump requirements
|
||||||
|
|
||||||
|
## v3.3.24 (2025/06/04)
|
||||||
|
* Remove hidden character
|
||||||
|
|
||||||
|
## v3.3.23 (2025/06/04)
|
||||||
|
* Update base image to bookworm. Thanks @rwjack
|
||||||
|
|
||||||
|
## v3.3.22 (2025/06/03)
|
||||||
|
* Disable search engine choice screen
|
||||||
|
* Fix headless=false stalling. Thanks @MAKMED1337
|
||||||
|
* Change from click to keys. Thanks @sh4dowb
|
||||||
|
* Don't open devtools
|
||||||
|
* Bump Chromium to v137 for build
|
||||||
|
* Bump requirements
|
||||||
|
|
||||||
## v3.3.21 (2024/06/26)
|
## v3.3.21 (2024/06/26)
|
||||||
* Add challenge selector to catch reloading page on non-English systems
|
* Add challenge selector to catch reloading page on non-English systems
|
||||||
* Escape values for generated form used in request.post. Thanks @mynameisbogdan
|
* Escape values for generated form used in request.post. Thanks @mynameisbogdan
|
||||||
|
|||||||
21
Dockerfile
21
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11-slim-bullseye as builder
|
FROM python:3.13-slim-bookworm AS builder
|
||||||
|
|
||||||
# Build dummy packages to skip installing them and their dependencies
|
# Build dummy packages to skip installing them and their dependencies
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
@@ -12,7 +12,7 @@ RUN apt-get update \
|
|||||||
&& equivs-build adwaita-icon-theme \
|
&& equivs-build adwaita-icon-theme \
|
||||||
&& mv adwaita-icon-theme_*.deb /adwaita-icon-theme.deb
|
&& mv adwaita-icon-theme_*.deb /adwaita-icon-theme.deb
|
||||||
|
|
||||||
FROM python:3.11-slim-bullseye
|
FROM python:3.13-slim-bookworm
|
||||||
|
|
||||||
# Copy dummy packages
|
# Copy dummy packages
|
||||||
COPY --from=builder /*.deb /
|
COPY --from=builder /*.deb /
|
||||||
@@ -38,7 +38,12 @@ RUN dpkg -i /libgl1-mesa-dri.deb \
|
|||||||
# Create flaresolverr user
|
# Create flaresolverr user
|
||||||
&& useradd --home-dir /app --shell /bin/sh flaresolverr \
|
&& useradd --home-dir /app --shell /bin/sh flaresolverr \
|
||||||
&& mv /usr/bin/chromedriver chromedriver \
|
&& mv /usr/bin/chromedriver chromedriver \
|
||||||
&& chown -R flaresolverr:flaresolverr .
|
&& chown -R flaresolverr:flaresolverr . \
|
||||||
|
# Create config dir
|
||||||
|
&& mkdir /config \
|
||||||
|
&& chown flaresolverr:flaresolverr /config
|
||||||
|
|
||||||
|
VOLUME /config
|
||||||
|
|
||||||
# Install Python dependencies
|
# Install Python dependencies
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
@@ -62,17 +67,17 @@ ENTRYPOINT ["/usr/bin/dumb-init", "--"]
|
|||||||
CMD ["/usr/local/bin/python", "-u", "/app/flaresolverr.py"]
|
CMD ["/usr/local/bin/python", "-u", "/app/flaresolverr.py"]
|
||||||
|
|
||||||
# Local build
|
# Local build
|
||||||
# docker build -t ngosang/flaresolverr:3.3.21 .
|
# docker build -t ngosang/flaresolverr:3.4.6 .
|
||||||
# docker run -p 8191:8191 ngosang/flaresolverr:3.3.21
|
# docker run -p 8191:8191 ngosang/flaresolverr:3.4.6
|
||||||
|
|
||||||
# Multi-arch build
|
# Multi-arch build
|
||||||
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||||
# docker buildx create --use
|
# docker buildx create --use
|
||||||
# docker buildx build -t ngosang/flaresolverr:3.3.21 --platform linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8 .
|
# docker buildx build -t ngosang/flaresolverr:3.4.6 --platform linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8 .
|
||||||
# add --push to publish in DockerHub
|
# add --push to publish in DockerHub
|
||||||
|
|
||||||
# Test multi-arch build
|
# Test multi-arch build
|
||||||
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
# docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||||
# docker buildx create --use
|
# docker buildx create --use
|
||||||
# docker buildx build -t ngosang/flaresolverr:3.3.21 --platform linux/arm/v7 --load .
|
# docker buildx build -t ngosang/flaresolverr:3.4.6 --platform linux/arm/v7 --load .
|
||||||
# docker run -p 8191:8191 --platform linux/arm/v7 ngosang/flaresolverr:3.3.21
|
# docker run -p 8191:8191 --platform linux/arm/v7 ngosang/flaresolverr:3.4.6
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023 Diego Heras (ngosang / ngosang@hotmail.es)
|
Copyright (c) 2025 Diego Heras (ngosang / ngosang@hotmail.es)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
212
README.md
212
README.md
@@ -33,13 +33,14 @@ It is recommended to install using a Docker container because the project depend
|
|||||||
already included within the image.
|
already included within the image.
|
||||||
|
|
||||||
Docker images are available in:
|
Docker images are available in:
|
||||||
* GitHub Registry => https://github.com/orgs/FlareSolverr/packages/container/package/flaresolverr
|
|
||||||
* DockerHub => https://hub.docker.com/r/flaresolverr/flaresolverr
|
- GitHub Registry => https://github.com/orgs/FlareSolverr/packages/container/package/flaresolverr
|
||||||
|
- DockerHub => https://hub.docker.com/r/flaresolverr/flaresolverr
|
||||||
|
|
||||||
Supported architectures are:
|
Supported architectures are:
|
||||||
|
|
||||||
| Architecture | Tag |
|
| Architecture | Tag |
|
||||||
|--------------|--------------|
|
| ------------ | ------------ |
|
||||||
| x86 | linux/386 |
|
| x86 | linux/386 |
|
||||||
| x86-64 | linux/amd64 |
|
| x86-64 | linux/amd64 |
|
||||||
| ARM32 | linux/arm/v7 |
|
| ARM32 | linux/arm/v7 |
|
||||||
@@ -50,6 +51,7 @@ We provide a `docker-compose.yml` configuration file. Clone this repository and
|
|||||||
the container.
|
the container.
|
||||||
|
|
||||||
If you prefer the `docker cli` execute the following command.
|
If you prefer the `docker cli` execute the following command.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name=flaresolverr \
|
--name=flaresolverr \
|
||||||
@@ -59,7 +61,7 @@ docker run -d \
|
|||||||
ghcr.io/flaresolverr/flaresolverr:latest
|
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`
|
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`.
|
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.
|
Remember to restart the Docker daemon and the container after the update.
|
||||||
|
|
||||||
@@ -69,28 +71,29 @@ Remember to restart the Docker daemon and the container after the update.
|
|||||||
> Precompiled binaries are only available for x64 architecture. For other architectures see Docker images.
|
> Precompiled binaries are only available for x64 architecture. For other architectures see Docker images.
|
||||||
|
|
||||||
This is the recommended way for Windows users.
|
This is the recommended way for Windows users.
|
||||||
* Download the [FlareSolverr executable](https://github.com/FlareSolverr/FlareSolverr/releases) from the release's page. It is available for Windows x64 and Linux x64.
|
|
||||||
* Execute FlareSolverr binary. In the environment variables section you can find how to change the configuration.
|
- Download the [FlareSolverr executable](https://github.com/FlareSolverr/FlareSolverr/releases) from the release's page. It is available for Windows x64 and Linux x64.
|
||||||
|
- Execute FlareSolverr binary. In the environment variables section you can find how to change the configuration.
|
||||||
|
|
||||||
### From source code
|
### From source code
|
||||||
|
|
||||||
> **Warning**
|
> **Warning**
|
||||||
> Installing from source code only works for x64 architecture. For other architectures see Docker images.
|
> Installing from source code only works for x64 architecture. For other architectures see Docker images.
|
||||||
|
|
||||||
* Install [Python 3.11](https://www.python.org/downloads/).
|
- Install [Python 3.13](https://www.python.org/downloads/).
|
||||||
* Install [Chrome](https://www.google.com/intl/en_us/chrome/) (all OS) or [Chromium](https://www.chromium.org/getting-involved/download-chromium/) (just Linux, it doesn't work in Windows) web browser.
|
- Install [Chrome](https://www.google.com/intl/en_us/chrome/) (all OS) or [Chromium](https://www.chromium.org/getting-involved/download-chromium/) (just Linux, it doesn't work in Windows) web browser.
|
||||||
* (Only in Linux) Install [Xvfb](https://en.wikipedia.org/wiki/Xvfb) package.
|
- (Only in Linux) Install [Xvfb](https://en.wikipedia.org/wiki/Xvfb) package.
|
||||||
* (Only in macOS) Install [XQuartz](https://www.xquartz.org/) package.
|
- (Only in macOS) Install [XQuartz](https://www.xquartz.org/) package.
|
||||||
* Clone this repository and open a shell in that path.
|
- Clone this repository and open a shell in that path.
|
||||||
* Run `pip install -r requirements.txt` command to install FlareSolverr dependencies.
|
- Run `pip install -r requirements.txt` command to install FlareSolverr dependencies.
|
||||||
* Run `python src/flaresolverr.py` command to start FlareSolverr.
|
- Run `python src/flaresolverr.py` command to start FlareSolverr.
|
||||||
|
|
||||||
### From source code (FreeBSD/TrueNAS CORE)
|
### From source code (FreeBSD/TrueNAS CORE)
|
||||||
|
|
||||||
* Run `pkg install chromium python39 py39-pip xorg-vfbserver` command to install the required dependencies.
|
- Run `pkg install chromium python313 py313-pip xorg-vfbserver` command to install the required dependencies.
|
||||||
* Clone this repository and open a shell in that path.
|
- Clone this repository and open a shell in that path.
|
||||||
* Run `python3.9 -m pip install -r requirements.txt` command to install FlareSolverr dependencies.
|
- Run `python3.13 -m pip install -r requirements.txt` command to install FlareSolverr dependencies.
|
||||||
* Run `python3.9 src/flaresolverr.py` command to start FlareSolverr.
|
- Run `python3.13 src/flaresolverr.py` command to start FlareSolverr.
|
||||||
|
|
||||||
### Systemd service
|
### Systemd service
|
||||||
|
|
||||||
@@ -99,6 +102,7 @@ We provide an example Systemd unit file `flaresolverr.service` as reference. You
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Example Bash request:
|
Example Bash request:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -L -X POST 'http://localhost:8191/v1' \
|
curl -L -X POST 'http://localhost:8191/v1' \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
@@ -110,6 +114,7 @@ curl -L -X POST 'http://localhost:8191/v1' \
|
|||||||
```
|
```
|
||||||
|
|
||||||
Example Python request:
|
Example Python request:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -125,6 +130,7 @@ print(response.text)
|
|||||||
```
|
```
|
||||||
|
|
||||||
Example PowerShell request:
|
Example PowerShell request:
|
||||||
|
|
||||||
```ps1
|
```ps1
|
||||||
$body = @{
|
$body = @{
|
||||||
cmd = "request.get"
|
cmd = "request.get"
|
||||||
@@ -145,9 +151,9 @@ cookies for the browser to use.
|
|||||||
|
|
||||||
This also speeds up the requests since it won't have to launch a new browser instance for every request.
|
This also speeds up the requests since it won't have to launch a new browser instance for every request.
|
||||||
|
|
||||||
| Parameter | Notes |
|
| 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. |
|
| 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 supported. Eg: `"proxy": {"url": "http://127.0.0.1:8888", "username": "testuser", "password": "testpass"}` |
|
| 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 supported. Eg: `"proxy": {"url": "http://127.0.0.1:8888", "username": "testuser", "password": "testpass"}` |
|
||||||
|
|
||||||
#### + `sessions.list`
|
#### + `sessions.list`
|
||||||
@@ -160,11 +166,7 @@ Example response:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"sessions": [
|
"sessions": ["session_id_1", "session_id_2", "session_id_3..."]
|
||||||
"session_id_1",
|
|
||||||
"session_id_2",
|
|
||||||
"session_id_3..."
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -174,20 +176,24 @@ This will properly shutdown a browser instance and remove all files associated w
|
|||||||
session. When you no longer need to use a session you should make sure to close it.
|
session. When you no longer need to use a session you should make sure to close it.
|
||||||
|
|
||||||
| Parameter | Notes |
|
| Parameter | Notes |
|
||||||
|-----------|-----------------------------------------------|
|
| --------- | --------------------------------------------- |
|
||||||
| session | The session ID that you want to be destroyed. |
|
| session | The session ID that you want to be destroyed. |
|
||||||
|
|
||||||
#### + `request.get`
|
#### + `request.get`
|
||||||
|
|
||||||
| Parameter | Notes |
|
| Parameter | Notes |
|
||||||
|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| url | Mandatory |
|
| url | Mandatory |
|
||||||
| session | Optional. Will send the request from and existing browser instance. If one is not sent it will create a temporary instance that will be destroyed immediately after the request is completed. |
|
| session | Optional. Will send the request from and existing browser instance. If one is not sent it will create a temporary instance that will be destroyed immediately after the request is completed. |
|
||||||
| session_ttl_minutes | Optional. FlareSolverr will automatically rotate expired sessions based on the TTL provided in minutes. |
|
| session_ttl_minutes | Optional. FlareSolverr will automatically rotate expired sessions based on the TTL provided in minutes. |
|
||||||
| 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. Eg: `"cookies": [{"name": "cookie1", "value": "value1"}, {"name": "cookie2", "value": "value2"}]`. |
|
| cookies | Optional. Will be used by the headless browser. Eg: `"cookies": [{"name": "cookie1", "value": "value1"}, {"name": "cookie2", "value": "value2"}]`. |
|
||||||
| 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. |
|
||||||
|
| returnScreenshot | Optional, default false. Captures a screenshot of the final rendered page after all challenges and waits are completed. The screenshot is returned as a Base64-encoded PNG string in the `screenshot` field of the response. |
|
||||||
| 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`.) |
|
| 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`.) |
|
||||||
|
| waitInSeconds | Optional, default none. Length to wait in seconds after solving the challenge, and before returning the results. Useful to allow it to load dynamic content. |
|
||||||
|
| disableMedia | Optional, default false. When true FlareSolverr will prevent media resources (images, CSS, and fonts) from being loaded to speed up navigation. |
|
||||||
|
| tabs_till_verify | Optional, default none. Number of times the `Tab` button is needed to be pressed to end up on the turnstile captcha, in order to verify it. After verifying the captcha, the result will be stored in the solution under `turnstile_token`. |
|
||||||
|
|
||||||
> **Warning**
|
> **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.
|
> 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.
|
||||||
@@ -196,96 +202,103 @@ Example response from running the `curl` above:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"solution": {
|
"solution": {
|
||||||
"url": "https://www.google.com/?gws_rd=ssl",
|
"url": "https://www.google.com/?gws_rd=ssl",
|
||||||
"status": 200,
|
"status": 200,
|
||||||
"headers": {
|
"headers": {
|
||||||
"status": "200",
|
"status": "200",
|
||||||
"date": "Thu, 16 Jul 2020 04:15:49 GMT",
|
"date": "Thu, 16 Jul 2020 04:15:49 GMT",
|
||||||
"expires": "-1",
|
"expires": "-1",
|
||||||
"cache-control": "private, max-age=0",
|
"cache-control": "private, max-age=0",
|
||||||
"content-type": "text/html; charset=UTF-8",
|
"content-type": "text/html; charset=UTF-8",
|
||||||
"strict-transport-security": "max-age=31536000",
|
"strict-transport-security": "max-age=31536000",
|
||||||
"p3p": "CP=\"This is not a P3P policy! See g.co/p3phelp for more info.\"",
|
"p3p": "CP=\"This is not a P3P policy! See g.co/p3phelp for more info.\"",
|
||||||
"content-encoding": "br",
|
"content-encoding": "br",
|
||||||
"server": "gws",
|
"server": "gws",
|
||||||
"content-length": "61587",
|
"content-length": "61587",
|
||||||
"x-xss-protection": "0",
|
"x-xss-protection": "0",
|
||||||
"x-frame-options": "SAMEORIGIN",
|
"x-frame-options": "SAMEORIGIN",
|
||||||
"set-cookie": "1P_JAR=2020-07-16-04; expires=Sat..."
|
"set-cookie": "1P_JAR=2020-07-16-04; expires=Sat..."
|
||||||
},
|
|
||||||
"response":"<!DOCTYPE html>...",
|
|
||||||
"cookies": [
|
|
||||||
{
|
|
||||||
"name": "NID",
|
|
||||||
"value": "204=QE3Ocq15XalczqjuDy52HeseG3zAZuJzID3R57...",
|
|
||||||
"domain": ".google.com",
|
|
||||||
"path": "/",
|
|
||||||
"expires": 1610684149.307722,
|
|
||||||
"size": 178,
|
|
||||||
"httpOnly": true,
|
|
||||||
"secure": true,
|
|
||||||
"session": false,
|
|
||||||
"sameSite": "None"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "1P_JAR",
|
|
||||||
"value": "2020-07-16-04",
|
|
||||||
"domain": ".google.com",
|
|
||||||
"path": "/",
|
|
||||||
"expires": 1597464949.307626,
|
|
||||||
"size": 19,
|
|
||||||
"httpOnly": false,
|
|
||||||
"secure": true,
|
|
||||||
"session": false,
|
|
||||||
"sameSite": "None"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"userAgent": "Windows NT 10.0; Win64; x64) AppleWebKit/5..."
|
|
||||||
},
|
},
|
||||||
"status": "ok",
|
"response": "<!DOCTYPE html>...",
|
||||||
"message": "",
|
"cookies": [
|
||||||
"startTimestamp": 1594872947467,
|
{
|
||||||
"endTimestamp": 1594872949617,
|
"name": "NID",
|
||||||
"version": "1.0.0"
|
"value": "204=QE3Ocq15XalczqjuDy52HeseG3zAZuJzID3R57...",
|
||||||
|
"domain": ".google.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1610684149.307722,
|
||||||
|
"size": 178,
|
||||||
|
"httpOnly": true,
|
||||||
|
"secure": true,
|
||||||
|
"session": false,
|
||||||
|
"sameSite": "None"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "1P_JAR",
|
||||||
|
"value": "2020-07-16-04",
|
||||||
|
"domain": ".google.com",
|
||||||
|
"path": "/",
|
||||||
|
"expires": 1597464949.307626,
|
||||||
|
"size": 19,
|
||||||
|
"httpOnly": false,
|
||||||
|
"secure": true,
|
||||||
|
"session": false,
|
||||||
|
"sameSite": "None"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"userAgent": "Windows NT 10.0; Win64; x64) AppleWebKit/5...",
|
||||||
|
"turnstile_token": "03AGdBq24k3lK7JH2v8uN1T5F..."
|
||||||
|
},
|
||||||
|
"status": "ok",
|
||||||
|
"message": "",
|
||||||
|
"startTimestamp": 1594872947467,
|
||||||
|
"endTimestamp": 1594872949617,
|
||||||
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### + `request.post`
|
### + `request.post`
|
||||||
|
|
||||||
This is the same as `request.get` but it takes one more param:
|
This works like `request.get`, with the addition of the postData parameter. Note that `tabs_till_verify` is currently supported only for GET requests and requires one extra argument.
|
||||||
|
|
||||||
| Parameter | Notes |
|
| Parameter | Notes |
|
||||||
|-----------|--------------------------------------------------------------------------|
|
| --------- | ------------------------------------------------------------------------ |
|
||||||
| postData | Must be a string with `application/x-www-form-urlencoded`. Eg: `a=b&c=d` |
|
| postData | Must be a string with `application/x-www-form-urlencoded`. Eg: `a=b&c=d` |
|
||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
| Name | Default | Notes |
|
| 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_FILE | none | Path to capture log to file. Example: `/config/flaresolverr.log`. |
|
||||||
| CAPTCHA_SOLVER | none | Captcha solving method. It is used when a captcha is encountered. See the Captcha Solvers section. |
|
| LOG_HTML | false | Only for debugging. If `true` all HTML that passes through the proxy will be logged to the console in `debug` level. |
|
||||||
| TZ | UTC | Timezone used in the logs and the web browser. Example: `TZ=Europe/London`. |
|
| PROXY_URL | none | URL for proxy. Will be overwritten by `request` or `sessions` proxy, if used. Example: `http://127.0.0.1:8080`. |
|
||||||
| LANG | none | Language used in the web browser. Example: `LANG=en_GB`. |
|
| PROXY_USERNAME | none | Username for proxy. Will be overwritten by `request` or `sessions` proxy, if used. Example: `testuser`. |
|
||||||
| HEADLESS | true | Only for debugging. To run the web browser in headless mode or visible. |
|
| PROXY_PASSWORD | none | Password for proxy. Will be overwritten by `request` or `sessions` proxy, if used. Example: `testpass`. |
|
||||||
| 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. |
|
| CAPTCHA_SOLVER | none | Captcha solving method. It is used when a captcha is encountered. See the Captcha Solvers section. |
|
||||||
| 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. |
|
| TZ | UTC | Timezone used in the logs and the web browser. Example: `TZ=Europe/London`. |
|
||||||
| PORT | 8191 | Listening port. You don't need to change this if you are running on Docker. |
|
| LANG | none | Language used in the web browser. Example: `LANG=en_GB`. |
|
||||||
| HOST | 0.0.0.0 | Listening interface. You don't need to change this if you are running on Docker. |
|
| HEADLESS | true | Only for debugging. To run the web browser in headless mode or visible. |
|
||||||
| PROMETHEUS_ENABLED | false | Enable Prometheus exporter. See the Prometheus section below. |
|
| DISABLE_MEDIA | false | To disable loading images, CSS, and other media in the web browser to save network bandwidth. |
|
||||||
| PROMETHEUS_PORT | 8192 | Listening port for Prometheus exporter. See the Prometheus section below. |
|
| 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. |
|
||||||
|
| PROMETHEUS_ENABLED | false | Enable Prometheus exporter. See the Prometheus section below. |
|
||||||
|
| PROMETHEUS_PORT | 8192 | Listening port for Prometheus exporter. See the Prometheus section below. |
|
||||||
|
|
||||||
Environment variables are set differently depending on the operating system. Some examples:
|
Environment variables are set differently depending on the operating system. Some examples:
|
||||||
* Docker: Take a look at the Docker section in this document. Environment variables can be set in the `docker-compose.yml` file or in the Docker CLI command.
|
|
||||||
* Linux: Run `export LOG_LEVEL=debug` and then run `flaresolverr` in the same shell.
|
- Docker: Take a look at the Docker section in this document. Environment variables can be set in the `docker-compose.yml` file or in the Docker CLI command.
|
||||||
* Windows: Open `cmd.exe`, run `set LOG_LEVEL=debug` and then run `flaresolverr.exe` in the same shell.
|
- Linux: Run `export LOG_LEVEL=debug` and then run `flaresolverr` in the same shell.
|
||||||
|
- Windows: Open `cmd.exe`, run `set LOG_LEVEL=debug` and then run `flaresolverr.exe` in the same shell.
|
||||||
|
|
||||||
## Prometheus exporter
|
## Prometheus exporter
|
||||||
|
|
||||||
The Prometheus exporter for FlareSolverr is disabled by default. It can be enabled with the environment variable `PROMETHEUS_ENABLED`. If you are using Docker make sure you expose the `PROMETHEUS_PORT`.
|
The Prometheus exporter for FlareSolverr is disabled by default. It can be enabled with the environment variable `PROMETHEUS_ENABLED`. If you are using Docker make sure you expose the `PROMETHEUS_PORT`.
|
||||||
|
|
||||||
Example metrics:
|
Example metrics:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# HELP flaresolverr_request_total Total requests with result
|
# HELP flaresolverr_request_total Total requests with result
|
||||||
# TYPE flaresolverr_request_total counter
|
# TYPE flaresolverr_request_total counter
|
||||||
@@ -317,8 +330,9 @@ solve a captcha.
|
|||||||
If this is the case, FlareSolverr will return the error `Captcha detected but no automatic solver is configured.`
|
If this is the case, FlareSolverr will return the error `Captcha detected but no automatic solver is configured.`
|
||||||
|
|
||||||
FlareSolverr can be customized to solve the CAPTCHA automatically by setting the environment variable `CAPTCHA_SOLVER`
|
FlareSolverr can be customized to solve the CAPTCHA automatically by setting the environment variable `CAPTCHA_SOLVER`
|
||||||
to the file name of one of the adapters inside the [/captcha](src/captcha) directory.
|
to the file name of one of the adapters inside the `/captcha` directory.
|
||||||
|
|
||||||
## Related projects
|
## Related projects
|
||||||
|
|
||||||
* C# implementation => https://github.com/FlareSolverr/FlareSolverrSharp
|
- C# implementation => https://github.com/FlareSolverr/FlareSolverrSharp
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ services:
|
|||||||
container_name: flaresolverr
|
container_name: flaresolverr
|
||||||
environment:
|
environment:
|
||||||
- LOG_LEVEL=${LOG_LEVEL:-info}
|
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
|
- LOG_FILE=${LOG_FILE:-none}
|
||||||
- LOG_HTML=${LOG_HTML:-false}
|
- LOG_HTML=${LOG_HTML:-false}
|
||||||
- CAPTCHA_SOLVER=${CAPTCHA_SOLVER:-none}
|
- CAPTCHA_SOLVER=${CAPTCHA_SOLVER:-none}
|
||||||
- TZ=Europe/London
|
- TZ=Europe/London
|
||||||
ports:
|
ports:
|
||||||
- "${PORT:-8191}:8191"
|
- "${PORT:-8191}:8191"
|
||||||
|
volumes:
|
||||||
|
- /var/lib/flaresolver:/config
|
||||||
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
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flaresolverr",
|
"name": "flaresolverr",
|
||||||
"version": "3.3.21",
|
"version": "3.4.6",
|
||||||
"description": "Proxy server to bypass Cloudflare protection",
|
"description": "Proxy server to bypass Cloudflare protection",
|
||||||
"author": "Diego Heras (ngosang / ngosang@hotmail.es)",
|
"author": "Diego Heras (ngosang / ngosang@hotmail.es)",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
bottle==0.12.25
|
bottle==0.13.4
|
||||||
waitress==2.1.2
|
waitress==3.0.2
|
||||||
selenium==4.15.2
|
selenium==4.38.0
|
||||||
func-timeout==4.3.5
|
func-timeout==4.3.5
|
||||||
prometheus-client==0.17.1
|
prometheus-client==0.23.1
|
||||||
# required by undetected_chromedriver
|
# Required by undetected_chromedriver
|
||||||
requests==2.32.0
|
requests==2.32.5
|
||||||
certifi==2024.07.04
|
certifi==2025.10.5
|
||||||
websockets==11.0.3
|
websockets==15.0.1
|
||||||
# only required for linux and macos
|
packaging==25.0
|
||||||
xvfbwrapper==0.2.9; platform_system != "Windows"
|
# Only required for Linux and macOS
|
||||||
# only required for windows
|
xvfbwrapper==0.2.15; platform_system != "Windows"
|
||||||
pefile==2023.2.7; platform_system == "Windows"
|
# Only required for Windows
|
||||||
|
pefile==2024.8.26; platform_system == "Windows"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import logging
|
|||||||
def logger_plugin(callback):
|
def logger_plugin(callback):
|
||||||
"""
|
"""
|
||||||
Bottle plugin to use logging module
|
Bottle plugin to use logging module
|
||||||
http://bottlepy.org/docs/dev/plugindev.html
|
https://bottlepy.org/docs/dev/plugindev.html
|
||||||
|
|
||||||
Wrap a Bottle request so that a log line is emitted after it's handled.
|
Wrap a Bottle request so that a log line is emitted after it's handled.
|
||||||
(This decorator can be extended to take the desired logger as a param.)
|
(This decorator can be extended to take the desired logger as a param.)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def setup():
|
|||||||
def prometheus_plugin(callback):
|
def prometheus_plugin(callback):
|
||||||
"""
|
"""
|
||||||
Bottle plugin to expose Prometheus metrics
|
Bottle plugin to expose Prometheus metrics
|
||||||
http://bottlepy.org/docs/dev/plugindev.html
|
https://bottlepy.org/docs/dev/plugindev.html
|
||||||
"""
|
"""
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
actual_response = callback(*args, **kwargs)
|
actual_response = callback(*args, **kwargs)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def clean_files():
|
|||||||
|
|
||||||
def download_chromium():
|
def download_chromium():
|
||||||
# https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/
|
# https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/
|
||||||
revision = "1260008" if os.name == 'nt' else '1260015'
|
revision = "1522586" if os.name == 'nt' else '1522586'
|
||||||
arch = 'Win_x64' if os.name == 'nt' else 'Linux_x64'
|
arch = 'Win_x64' if os.name == 'nt' else 'Linux_x64'
|
||||||
dl_file = 'chrome-win' if os.name == 'nt' else 'chrome-linux'
|
dl_file = 'chrome-win' if os.name == 'nt' else 'chrome-linux'
|
||||||
dl_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 'dist_chrome')
|
dl_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 'dist_chrome')
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class ChallengeResolutionResultT:
|
|||||||
response: str = None
|
response: str = None
|
||||||
cookies: list = None
|
cookies: list = None
|
||||||
userAgent: str = None
|
userAgent: str = None
|
||||||
|
screenshot: str | None = None
|
||||||
|
turnstile_token: str = None
|
||||||
|
|
||||||
def __init__(self, _dict):
|
def __init__(self, _dict):
|
||||||
self.__dict__.update(_dict)
|
self.__dict__.update(_dict)
|
||||||
@@ -41,8 +43,14 @@ class V1RequestBase(object):
|
|||||||
url: str = None
|
url: str = None
|
||||||
postData: str = None
|
postData: str = None
|
||||||
returnOnlyCookies: bool = None
|
returnOnlyCookies: bool = None
|
||||||
|
returnScreenshot: bool = None
|
||||||
download: bool = None # deprecated v2.0.0, not used
|
download: bool = None # deprecated v2.0.0, not used
|
||||||
returnRawHtml: bool = None # deprecated v2.0.0, not used
|
returnRawHtml: bool = None # deprecated v2.0.0, not used
|
||||||
|
waitInSeconds: int = None
|
||||||
|
# Optional resource blocking flag (blocks images, CSS, and fonts)
|
||||||
|
disableMedia: bool = None
|
||||||
|
# Optional when you've got a turnstile captcha that needs to be clicked after X number of Tab presses
|
||||||
|
tabs_till_verify : int = None
|
||||||
|
|
||||||
def __init__(self, _dict):
|
def __init__(self, _dict):
|
||||||
self.__dict__.update(_dict)
|
self.__dict__.update(_dict)
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ from dtos import V1RequestBase
|
|||||||
import flaresolverr_service
|
import flaresolverr_service
|
||||||
import utils
|
import utils
|
||||||
|
|
||||||
|
env_proxy_url = os.environ.get('PROXY_URL', None)
|
||||||
|
env_proxy_username = os.environ.get('PROXY_USERNAME', None)
|
||||||
|
env_proxy_password = os.environ.get('PROXY_PASSWORD', None)
|
||||||
|
|
||||||
|
|
||||||
class JSONErrorBottle(Bottle):
|
class JSONErrorBottle(Bottle):
|
||||||
"""
|
"""
|
||||||
@@ -50,7 +54,14 @@ def controller_v1():
|
|||||||
"""
|
"""
|
||||||
Controller v1
|
Controller v1
|
||||||
"""
|
"""
|
||||||
req = V1RequestBase(request.json)
|
data = request.json or {}
|
||||||
|
if (('proxy' not in data or not data.get('proxy')) and env_proxy_url is not None and (env_proxy_username is None and env_proxy_password is None)):
|
||||||
|
logging.info('Using proxy URL ENV')
|
||||||
|
data['proxy'] = {"url": env_proxy_url}
|
||||||
|
if (('proxy' not in data or not data.get('proxy')) and env_proxy_url is not None and (env_proxy_username is not None or env_proxy_password is not None)):
|
||||||
|
logging.info('Using proxy URL, username & password ENVs')
|
||||||
|
data['proxy'] = {"url": env_proxy_url, "username": env_proxy_username, "password": env_proxy_password}
|
||||||
|
req = V1RequestBase(data)
|
||||||
res = flaresolverr_service.controller_v1_endpoint(req)
|
res = flaresolverr_service.controller_v1_endpoint(req)
|
||||||
if res.__error_500__:
|
if res.__error_500__:
|
||||||
response.status = 500
|
response.status = 500
|
||||||
@@ -70,12 +81,13 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# fix ssl certificates for compiled binaries
|
# fix ssl certificates for compiled binaries
|
||||||
# https://github.com/pyinstaller/pyinstaller/issues/7229
|
# https://github.com/pyinstaller/pyinstaller/issues/7229
|
||||||
# https://stackoverflow.com/questions/55736855/how-to-change-the-cafile-argument-in-the-ssl-module-in-python3
|
# https://stackoverflow.com/q/55736855
|
||||||
os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
|
os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
|
||||||
os.environ["SSL_CERT_FILE"] = certifi.where()
|
os.environ["SSL_CERT_FILE"] = certifi.where()
|
||||||
|
|
||||||
# validate configuration
|
# validate configuration
|
||||||
log_level = os.environ.get('LOG_LEVEL', 'info').upper()
|
log_level = os.environ.get('LOG_LEVEL', 'info').upper()
|
||||||
|
log_file = os.environ.get('LOG_FILE', None)
|
||||||
log_html = utils.get_config_log_html()
|
log_html = utils.get_config_log_html()
|
||||||
headless = utils.get_config_headless()
|
headless = utils.get_config_headless()
|
||||||
server_host = os.environ.get('HOST', '0.0.0.0')
|
server_host = os.environ.get('HOST', '0.0.0.0')
|
||||||
@@ -85,14 +97,29 @@ if __name__ == "__main__":
|
|||||||
logger_format = '%(asctime)s %(levelname)-8s %(message)s'
|
logger_format = '%(asctime)s %(levelname)-8s %(message)s'
|
||||||
if log_level == 'DEBUG':
|
if log_level == 'DEBUG':
|
||||||
logger_format = '%(asctime)s %(levelname)-8s ReqId %(thread)s %(message)s'
|
logger_format = '%(asctime)s %(levelname)-8s ReqId %(thread)s %(message)s'
|
||||||
logging.basicConfig(
|
if log_file:
|
||||||
format=logger_format,
|
log_file = os.path.realpath(log_file)
|
||||||
level=log_level,
|
log_path = os.path.dirname(log_file)
|
||||||
datefmt='%Y-%m-%d %H:%M:%S',
|
os.makedirs(log_path, exist_ok=True)
|
||||||
handlers=[
|
logging.basicConfig(
|
||||||
logging.StreamHandler(sys.stdout)
|
format=logger_format,
|
||||||
]
|
level=log_level,
|
||||||
)
|
datefmt='%Y-%m-%d %H:%M:%S',
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(sys.stdout),
|
||||||
|
logging.FileHandler(log_file)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.basicConfig(
|
||||||
|
format=logger_format,
|
||||||
|
level=log_level,
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S',
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(sys.stdout)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# disable warning traces from urllib3
|
# disable warning traces from urllib3
|
||||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||||
logging.getLogger('selenium.webdriver.remote.remote_connection').setLevel(logging.WARNING)
|
logging.getLogger('selenium.webdriver.remote.remote_connection').setLevel(logging.WARNING)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from func_timeout import FunctionTimedOut, func_timeout
|
|||||||
from selenium.common import TimeoutException
|
from selenium.common import TimeoutException
|
||||||
from selenium.webdriver.chrome.webdriver import WebDriver
|
from selenium.webdriver.chrome.webdriver import WebDriver
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
from selenium.webdriver.support.expected_conditions import (
|
from selenium.webdriver.support.expected_conditions import (
|
||||||
presence_of_element_located, staleness_of, title_is)
|
presence_of_element_located, staleness_of, title_is)
|
||||||
from selenium.webdriver.common.action_chains import ActionChains
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
@@ -47,6 +48,11 @@ CHALLENGE_SELECTORS = [
|
|||||||
# Fairlane / pararius.com
|
# Fairlane / pararius.com
|
||||||
'div.vc div.text-box h2'
|
'div.vc div.text-box h2'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
TURNSTILE_SELECTORS = [
|
||||||
|
"input[name='cf-turnstile-response']"
|
||||||
|
]
|
||||||
|
|
||||||
SHORT_TIMEOUT = 1
|
SHORT_TIMEOUT = 1
|
||||||
SESSIONS_STORAGE = SessionsStorage()
|
SESSIONS_STORAGE = SessionsStorage()
|
||||||
|
|
||||||
@@ -252,21 +258,17 @@ def _resolve_challenge(req: V1RequestBase, method: str) -> ChallengeResolutionT:
|
|||||||
logging.debug('A used instance of webdriver has been destroyed')
|
logging.debug('A used instance of webdriver has been destroyed')
|
||||||
|
|
||||||
|
|
||||||
def click_verify(driver: WebDriver):
|
def click_verify(driver: WebDriver, num_tabs: int = 1):
|
||||||
try:
|
try:
|
||||||
logging.debug("Try to find the Cloudflare verify checkbox...")
|
logging.debug("Try to find the Cloudflare verify checkbox...")
|
||||||
iframe = driver.find_element(By.XPATH, "//iframe[starts-with(@id, 'cf-chl-widget-')]")
|
actions = ActionChains(driver)
|
||||||
driver.switch_to.frame(iframe)
|
actions.pause(5)
|
||||||
checkbox = driver.find_element(
|
for _ in range(num_tabs):
|
||||||
by=By.XPATH,
|
actions.send_keys(Keys.TAB).pause(0.1)
|
||||||
value='//*[@id="content"]/div/div/label/input',
|
actions.pause(1)
|
||||||
)
|
actions.send_keys(Keys.SPACE).perform()
|
||||||
if checkbox:
|
|
||||||
actions = ActionChains(driver)
|
logging.debug(f"Cloudflare verify checkbox clicked after {num_tabs} tabs!")
|
||||||
actions.move_to_element_with_offset(checkbox, 5, 7)
|
|
||||||
actions.click(checkbox)
|
|
||||||
actions.perform()
|
|
||||||
logging.debug("Cloudflare verify checkbox found and clicked!")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.debug("Cloudflare verify checkbox not found on the page.")
|
logging.debug("Cloudflare verify checkbox not found on the page.")
|
||||||
finally:
|
finally:
|
||||||
@@ -289,36 +291,90 @@ def click_verify(driver: WebDriver):
|
|||||||
|
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
|
def _get_turnstile_token(driver: WebDriver, tabs: int):
|
||||||
|
token_input = driver.find_element(By.CSS_SELECTOR, "input[name='cf-turnstile-response']")
|
||||||
|
current_value = token_input.get_attribute("value")
|
||||||
|
while True:
|
||||||
|
click_verify(driver, num_tabs=tabs)
|
||||||
|
turnstile_token = token_input.get_attribute("value")
|
||||||
|
if turnstile_token:
|
||||||
|
if turnstile_token != current_value:
|
||||||
|
logging.info(f"Turnstile token: {turnstile_token}")
|
||||||
|
return turnstile_token
|
||||||
|
logging.debug(f"Failed to extract token possibly click failed")
|
||||||
|
|
||||||
def get_correct_window(driver: WebDriver) -> WebDriver:
|
# reset focus
|
||||||
if len(driver.window_handles) > 1:
|
driver.execute_script("""
|
||||||
for window_handle in driver.window_handles:
|
let el = document.createElement('button');
|
||||||
driver.switch_to.window(window_handle)
|
el.style.position='fixed';
|
||||||
current_url = driver.current_url
|
el.style.top='0';
|
||||||
if not current_url.startswith("devtools://devtools"):
|
el.style.left='0';
|
||||||
return driver
|
document.body.prepend(el);
|
||||||
return driver
|
el.focus();
|
||||||
|
""")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def _resolve_turnstile_captcha(req: V1RequestBase, driver: WebDriver):
|
||||||
|
turnstile_token = None
|
||||||
|
if req.tabs_till_verify is not None:
|
||||||
|
logging.debug(f'Navigating to... {req.url} in order to pass the turnstile challenge')
|
||||||
|
driver.get(req.url)
|
||||||
|
|
||||||
def access_page(driver: WebDriver, url: str) -> None:
|
turnstile_challenge_found = False
|
||||||
driver.get(url)
|
for selector in TURNSTILE_SELECTORS:
|
||||||
driver.start_session()
|
found_elements = driver.find_elements(By.CSS_SELECTOR, selector)
|
||||||
driver.start_session() # required to bypass Cloudflare
|
if len(found_elements) > 0:
|
||||||
|
turnstile_challenge_found = True
|
||||||
|
logging.info("Turnstile challenge detected. Selector found: " + selector)
|
||||||
|
break
|
||||||
|
if turnstile_challenge_found:
|
||||||
|
turnstile_token = _get_turnstile_token(driver=driver, tabs=req.tabs_till_verify)
|
||||||
|
else:
|
||||||
|
logging.debug(f'Turnstile challenge not found')
|
||||||
|
return turnstile_token
|
||||||
|
|
||||||
def _evil_logic(req: V1RequestBase, driver: WebDriver, method: str) -> ChallengeResolutionT:
|
def _evil_logic(req: V1RequestBase, driver: WebDriver, method: str) -> ChallengeResolutionT:
|
||||||
res = ChallengeResolutionT({})
|
res = ChallengeResolutionT({})
|
||||||
res.status = STATUS_OK
|
res.status = STATUS_OK
|
||||||
res.message = ""
|
res.message = ""
|
||||||
|
|
||||||
|
# optionally block resources like images/css/fonts using CDP
|
||||||
|
disable_media = utils.get_config_disable_media()
|
||||||
|
if req.disableMedia is not None:
|
||||||
|
disable_media = req.disableMedia
|
||||||
|
if disable_media:
|
||||||
|
block_urls = [
|
||||||
|
# Images
|
||||||
|
"*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp", "*.bmp", "*.svg", "*.ico",
|
||||||
|
"*.PNG", "*.JPG", "*.JPEG", "*.GIF", "*.WEBP", "*.BMP", "*.SVG", "*.ICO",
|
||||||
|
"*.tiff", "*.tif", "*.jpe", "*.apng", "*.avif", "*.heic", "*.heif",
|
||||||
|
"*.TIFF", "*.TIF", "*.JPE", "*.APNG", "*.AVIF", "*.HEIC", "*.HEIF",
|
||||||
|
# Stylesheets
|
||||||
|
"*.css",
|
||||||
|
"*.CSS",
|
||||||
|
# Fonts
|
||||||
|
"*.woff", "*.woff2", "*.ttf", "*.otf", "*.eot",
|
||||||
|
"*.WOFF", "*.WOFF2", "*.TTF", "*.OTF", "*.EOT"
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
logging.debug("Network.setBlockedURLs: %s", block_urls)
|
||||||
|
driver.execute_cdp_cmd("Network.enable", {})
|
||||||
|
driver.execute_cdp_cmd("Network.setBlockedURLs", {"urls": block_urls})
|
||||||
|
except Exception:
|
||||||
|
# if CDP commands are not available or fail, ignore and continue
|
||||||
|
logging.debug("Network.setBlockedURLs failed or unsupported on this webdriver")
|
||||||
|
|
||||||
# navigate to the page
|
# navigate to the page
|
||||||
logging.debug(f'Navigating to... {req.url}')
|
logging.debug(f"Navigating to... {req.url}")
|
||||||
if method == 'POST':
|
turnstile_token = None
|
||||||
|
|
||||||
|
if method == "POST":
|
||||||
_post_request(req, driver)
|
_post_request(req, driver)
|
||||||
else:
|
else:
|
||||||
access_page(driver, req.url)
|
if req.tabs_till_verify is None:
|
||||||
driver = get_correct_window(driver)
|
driver.get(req.url)
|
||||||
|
else:
|
||||||
|
turnstile_token = _resolve_turnstile_captcha(req, driver)
|
||||||
|
|
||||||
# set cookies if required
|
# set cookies if required
|
||||||
if req.cookies is not None and len(req.cookies) > 0:
|
if req.cookies is not None and len(req.cookies) > 0:
|
||||||
@@ -330,8 +386,7 @@ def _evil_logic(req: V1RequestBase, driver: WebDriver, method: str) -> Challenge
|
|||||||
if method == 'POST':
|
if method == 'POST':
|
||||||
_post_request(req, driver)
|
_post_request(req, driver)
|
||||||
else:
|
else:
|
||||||
access_page(driver, req.url)
|
driver.get(req.url)
|
||||||
driver = get_correct_window(driver)
|
|
||||||
|
|
||||||
# wait for the page
|
# wait for the page
|
||||||
if utils.get_config_log_html():
|
if utils.get_config_log_html():
|
||||||
@@ -341,7 +396,7 @@ def _evil_logic(req: V1RequestBase, driver: WebDriver, method: str) -> Challenge
|
|||||||
|
|
||||||
# find access denied titles
|
# find access denied titles
|
||||||
for title in ACCESS_DENIED_TITLES:
|
for title in ACCESS_DENIED_TITLES:
|
||||||
if title == page_title:
|
if page_title.startswith(title):
|
||||||
raise Exception('Cloudflare has blocked this request. '
|
raise Exception('Cloudflare has blocked this request. '
|
||||||
'Probably your IP is banned for this site, check in your web browser.')
|
'Probably your IP is banned for this site, check in your web browser.')
|
||||||
# find access denied selectors
|
# find access denied selectors
|
||||||
@@ -413,21 +468,30 @@ def _evil_logic(req: V1RequestBase, driver: WebDriver, method: str) -> Challenge
|
|||||||
challenge_res.status = 200 # todo: fix, selenium not provides this info
|
challenge_res.status = 200 # todo: fix, selenium not provides this info
|
||||||
challenge_res.cookies = driver.get_cookies()
|
challenge_res.cookies = driver.get_cookies()
|
||||||
challenge_res.userAgent = utils.get_user_agent(driver)
|
challenge_res.userAgent = utils.get_user_agent(driver)
|
||||||
|
challenge_res.turnstile_token = turnstile_token
|
||||||
|
|
||||||
if not req.returnOnlyCookies:
|
if not req.returnOnlyCookies:
|
||||||
challenge_res.headers = {} # todo: fix, selenium not provides this info
|
challenge_res.headers = {} # todo: fix, selenium not provides this info
|
||||||
|
|
||||||
|
if req.waitInSeconds and req.waitInSeconds > 0:
|
||||||
|
logging.info("Waiting " + str(req.waitInSeconds) + " seconds before returning the response...")
|
||||||
|
time.sleep(req.waitInSeconds)
|
||||||
|
|
||||||
challenge_res.response = driver.page_source
|
challenge_res.response = driver.page_source
|
||||||
|
|
||||||
|
if req.returnScreenshot:
|
||||||
|
challenge_res.screenshot = driver.get_screenshot_as_base64()
|
||||||
|
|
||||||
res.result = challenge_res
|
res.result = challenge_res
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def _post_request(req: V1RequestBase, driver: WebDriver):
|
def _post_request(req: V1RequestBase, driver: WebDriver):
|
||||||
post_form = f'<form id="hackForm" action="{req.url}" method="POST">'
|
post_form = f'<form id="hackForm" action="{req.url}" method="POST">'
|
||||||
query_string = req.postData if req.postData[0] != '?' else req.postData[1:]
|
query_string = req.postData if req.postData and req.postData[0] != '?' else req.postData[1:] if req.postData else ''
|
||||||
pairs = query_string.split('&')
|
pairs = query_string.split('&')
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
parts = pair.split('=')
|
parts = pair.split('=', 1)
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
name = unquote(parts[0])
|
name = unquote(parts[0])
|
||||||
@@ -437,9 +501,11 @@ def _post_request(req: V1RequestBase, driver: WebDriver):
|
|||||||
continue
|
continue
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
value = unquote(parts[1])
|
value = unquote(parts[1]) if len(parts) > 1 else ''
|
||||||
except Exception:
|
except Exception:
|
||||||
value = parts[1]
|
value = parts[1] if len(parts) > 1 else ''
|
||||||
|
# Protection of " character, for syntax
|
||||||
|
value=value.replace('"','"')
|
||||||
post_form += f'<input type="text" name="{escape(quote(name))}" value="{escape(quote(value))}"><br>'
|
post_form += f'<input type="text" name="{escape(quote(name))}" value="{escape(quote(value))}"><br>'
|
||||||
post_form += '</form>'
|
post_form += '</form>'
|
||||||
html_content = f"""
|
html_content = f"""
|
||||||
@@ -451,5 +517,3 @@ def _post_request(req: V1RequestBase, driver: WebDriver):
|
|||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>"""
|
||||||
driver.get("data:text/html;charset=utf-8,{html_content}".format(html_content=html_content))
|
driver.get("data:text/html;charset=utf-8,{html_content}".format(html_content=html_content))
|
||||||
driver.start_session()
|
|
||||||
driver.start_session() # required to bypass Cloudflare
|
|
||||||
|
|||||||
31
src/tests.py
31
src/tests.py
@@ -21,11 +21,11 @@ class TestFlareSolverr(unittest.TestCase):
|
|||||||
proxy_socks_url = "socks5://127.0.0.1:1080"
|
proxy_socks_url = "socks5://127.0.0.1:1080"
|
||||||
google_url = "https://www.google.com"
|
google_url = "https://www.google.com"
|
||||||
post_url = "https://httpbin.org/post"
|
post_url = "https://httpbin.org/post"
|
||||||
cloudflare_url = "https://nowsecure.nl"
|
cloudflare_url = "https://nowsecure.nl/"
|
||||||
cloudflare_url_2 = "https://idope.se/torrent-list/harry/"
|
cloudflare_url_2 = "https://idope.se/torrent-list/harry/"
|
||||||
ddos_guard_url = "https://anidex.info/"
|
ddos_guard_url = "https://www.litres.ru/"
|
||||||
fairlane_url = "https://www.pararius.com/apartments/amsterdam"
|
fairlane_url = "https://www.pararius.com/apartments/amsterdam"
|
||||||
custom_cloudflare_url = "https://www.muziekfabriek.org"
|
custom_cloudflare_url = "https://www.muziekfabriek.org/"
|
||||||
cloudflare_blocked_url = "https://cpasbiens3.fr/index.php?do=search&subaction=search"
|
cloudflare_blocked_url = "https://cpasbiens3.fr/index.php?do=search&subaction=search"
|
||||||
|
|
||||||
app = TestApp(flaresolverr.app)
|
app = TestApp(flaresolverr.app)
|
||||||
@@ -92,6 +92,29 @@ class TestFlareSolverr(unittest.TestCase):
|
|||||||
self.assertGreater(len(solution.cookies), 0)
|
self.assertGreater(len(solution.cookies), 0)
|
||||||
self.assertIn("Chrome/", solution.userAgent)
|
self.assertIn("Chrome/", solution.userAgent)
|
||||||
|
|
||||||
|
def test_v1_endpoint_request_get_disable_resources(self):
|
||||||
|
res = self.app.post_json("/v1", {
|
||||||
|
"cmd": "request.get",
|
||||||
|
"url": self.google_url,
|
||||||
|
"disableMedia": True
|
||||||
|
})
|
||||||
|
self.assertEqual(res.status_code, 200)
|
||||||
|
|
||||||
|
body = V1ResponseBase(res.json)
|
||||||
|
self.assertEqual(STATUS_OK, body.status)
|
||||||
|
self.assertEqual("Challenge not detected!", body.message)
|
||||||
|
self.assertGreater(body.startTimestamp, 10000)
|
||||||
|
self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
|
||||||
|
self.assertEqual(utils.get_flaresolverr_version(), body.version)
|
||||||
|
|
||||||
|
solution = body.solution
|
||||||
|
self.assertIn(self.google_url, solution.url)
|
||||||
|
self.assertEqual(solution.status, 200)
|
||||||
|
self.assertIs(len(solution.headers), 0)
|
||||||
|
self.assertIn("<title>Google</title>", solution.response)
|
||||||
|
self.assertGreater(len(solution.cookies), 0)
|
||||||
|
self.assertIn("Chrome/", solution.userAgent)
|
||||||
|
|
||||||
def test_v1_endpoint_request_get_cloudflare_js_1(self):
|
def test_v1_endpoint_request_get_cloudflare_js_1(self):
|
||||||
res = self.app.post_json('/v1', {
|
res = self.app.post_json('/v1', {
|
||||||
"cmd": "request.get",
|
"cmd": "request.get",
|
||||||
@@ -162,7 +185,7 @@ class TestFlareSolverr(unittest.TestCase):
|
|||||||
self.assertIn(self.ddos_guard_url, solution.url)
|
self.assertIn(self.ddos_guard_url, solution.url)
|
||||||
self.assertEqual(solution.status, 200)
|
self.assertEqual(solution.status, 200)
|
||||||
self.assertIs(len(solution.headers), 0)
|
self.assertIs(len(solution.headers), 0)
|
||||||
self.assertIn("<title>AniDex</title>", solution.response)
|
self.assertIn("<title>Литрес", solution.response)
|
||||||
self.assertGreater(len(solution.cookies), 0)
|
self.assertGreater(len(solution.cookies), 0)
|
||||||
self.assertIn("Chrome/", solution.userAgent)
|
self.assertIn("Chrome/", solution.userAgent)
|
||||||
|
|
||||||
|
|||||||
@@ -471,7 +471,7 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
|
|||||||
self.patcher.executable_path
|
self.patcher.executable_path
|
||||||
)
|
)
|
||||||
|
|
||||||
super(Chrome, self).__init__(
|
super().__init__(
|
||||||
service=service,
|
service=service,
|
||||||
options=options,
|
options=options,
|
||||||
keep_alive=keep_alive,
|
keep_alive=keep_alive,
|
||||||
@@ -507,8 +507,6 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
|
|||||||
"Page.addScriptToEvaluateOnNewDocument",
|
"Page.addScriptToEvaluateOnNewDocument",
|
||||||
{
|
{
|
||||||
"source": """
|
"source": """
|
||||||
|
|
||||||
Object.defineProperty(window, "navigator", {
|
|
||||||
Object.defineProperty(window, "navigator", {
|
Object.defineProperty(window, "navigator", {
|
||||||
value: new Proxy(navigator, {
|
value: new Proxy(navigator, {
|
||||||
has: (target, key) => (key === "webdriver" ? false : key in target),
|
has: (target, key) => (key === "webdriver" ? false : key in target),
|
||||||
@@ -729,10 +727,8 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
|
|||||||
def start_session(self, capabilities=None, browser_profile=None):
|
def start_session(self, capabilities=None, browser_profile=None):
|
||||||
if not capabilities:
|
if not capabilities:
|
||||||
capabilities = self.options.to_capabilities()
|
capabilities = self.options.to_capabilities()
|
||||||
super(selenium.webdriver.chrome.webdriver.WebDriver, self).start_session(
|
super().start_session(capabilities)
|
||||||
capabilities
|
# super(Chrome, self).start_session(capabilities, browser_profile) # Original explicit call commented out
|
||||||
)
|
|
||||||
# super(Chrome, self).start_session(capabilities, browser_profile)
|
|
||||||
|
|
||||||
def find_elements_recursive(self, by, value):
|
def find_elements_recursive(self, by, value):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# this module is part of undetected_chromedriver
|
# this module is part of undetected_chromedriver
|
||||||
|
|
||||||
from distutils.version import LooseVersion
|
from packaging.version import Version as LooseVersion
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -12,6 +12,7 @@ import random
|
|||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import string
|
import string
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
@@ -222,7 +223,7 @@ class Patcher(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
release = self.fetch_release_number()
|
release = self.fetch_release_number()
|
||||||
self.version_main = release.version[0]
|
self.version_main = release.major
|
||||||
self.version_full = release
|
self.version_full = release
|
||||||
self.unzip_package(self.fetch_package())
|
self.unzip_package(self.fetch_package())
|
||||||
|
|
||||||
@@ -327,11 +328,11 @@ class Patcher(object):
|
|||||||
"""
|
"""
|
||||||
zip_name = f"chromedriver_{self.platform_name}.zip"
|
zip_name = f"chromedriver_{self.platform_name}.zip"
|
||||||
if self.is_old_chromedriver:
|
if self.is_old_chromedriver:
|
||||||
download_url = "%s/%s/%s" % (self.url_repo, self.version_full.vstring, zip_name)
|
download_url = "%s/%s/%s" % (self.url_repo, str(self.version_full), zip_name)
|
||||||
else:
|
else:
|
||||||
zip_name = zip_name.replace("_", "-", 1)
|
zip_name = zip_name.replace("_", "-", 1)
|
||||||
download_url = "https://storage.googleapis.com/chrome-for-testing-public/%s/%s/%s"
|
download_url = "https://storage.googleapis.com/chrome-for-testing-public/%s/%s/%s"
|
||||||
download_url %= (self.version_full.vstring, self.platform_name, zip_name)
|
download_url %= (str(self.version_full), self.platform_name, zip_name)
|
||||||
|
|
||||||
logger.debug("downloading from %s" % download_url)
|
logger.debug("downloading from %s" % download_url)
|
||||||
return urlretrieve(download_url)[0]
|
return urlretrieve(download_url)[0]
|
||||||
@@ -373,10 +374,31 @@ class Patcher(object):
|
|||||||
"""
|
"""
|
||||||
exe_name = os.path.basename(exe_name)
|
exe_name = os.path.basename(exe_name)
|
||||||
if IS_POSIX:
|
if IS_POSIX:
|
||||||
r = os.system("kill -f -9 $(pidof %s)" % exe_name)
|
# Using shell=True for pidof, consider a more robust pid finding method if issues arise.
|
||||||
|
# pgrep can be an alternative: ["pgrep", "-f", exe_name]
|
||||||
|
# Or psutil if adding a dependency is acceptable.
|
||||||
|
command = f"pidof {exe_name}"
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, shell=True, capture_output=True, text=True, check=True)
|
||||||
|
pids = result.stdout.strip().split()
|
||||||
|
if pids:
|
||||||
|
subprocess.run(["kill", "-9"] + pids, check=False) # Changed from -f -9 to -9 as -f is not standard for kill
|
||||||
|
return True
|
||||||
|
return False # No PIDs found
|
||||||
|
except subprocess.CalledProcessError: # pidof returns 1 if no process found
|
||||||
|
return False # No process found
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error killing process on POSIX: {e}")
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
r = os.system("taskkill /f /im %s" % exe_name)
|
try:
|
||||||
return not r
|
# TASKKILL /F /IM chromedriver.exe
|
||||||
|
result = subprocess.run(["taskkill", "/f", "/im", exe_name], check=False, capture_output=True)
|
||||||
|
# taskkill returns 0 if process was killed, 128 if not found.
|
||||||
|
return result.returncode == 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error killing process on Windows: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def gen_random_cdc():
|
def gen_random_cdc():
|
||||||
|
|||||||
40
src/utils.py
40
src/utils.py
@@ -1,11 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import urllib.parse
|
|
||||||
import tempfile
|
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from selenium.webdriver.chrome.webdriver import WebDriver
|
from selenium.webdriver.chrome.webdriver import WebDriver
|
||||||
import undetected_chromedriver as uc
|
import undetected_chromedriver as uc
|
||||||
@@ -27,6 +28,10 @@ def get_config_headless() -> bool:
|
|||||||
return os.environ.get('HEADLESS', 'true').lower() == 'true'
|
return os.environ.get('HEADLESS', 'true').lower() == 'true'
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_disable_media() -> bool:
|
||||||
|
return os.environ.get('DISABLE_MEDIA', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
|
||||||
def get_flaresolverr_version() -> str:
|
def get_flaresolverr_version() -> str:
|
||||||
global FLARESOLVERR_VERSION
|
global FLARESOLVERR_VERSION
|
||||||
if FLARESOLVERR_VERSION is not None:
|
if FLARESOLVERR_VERSION is not None:
|
||||||
@@ -57,18 +62,21 @@ def create_proxy_extension(proxy: dict) -> str:
|
|||||||
manifest_json = """
|
manifest_json = """
|
||||||
{
|
{
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"manifest_version": 2,
|
"manifest_version": 3,
|
||||||
"name": "Chrome Proxy",
|
"name": "Chrome Proxy",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"proxy",
|
"proxy",
|
||||||
"tabs",
|
"tabs",
|
||||||
"unlimitedStorage",
|
|
||||||
"storage",
|
"storage",
|
||||||
"<all_urls>",
|
|
||||||
"webRequest",
|
"webRequest",
|
||||||
"webRequestBlocking"
|
"webRequestAuthProvider"
|
||||||
],
|
],
|
||||||
"background": {"scripts": ["background.js"]},
|
"host_permissions": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
"minimum_chrome_version": "76.0.0"
|
"minimum_chrome_version": "76.0.0"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -129,21 +137,18 @@ def get_webdriver(proxy: dict = None) -> WebDriver:
|
|||||||
options = uc.ChromeOptions()
|
options = uc.ChromeOptions()
|
||||||
options.add_argument('--no-sandbox')
|
options.add_argument('--no-sandbox')
|
||||||
options.add_argument('--window-size=1920,1080')
|
options.add_argument('--window-size=1920,1080')
|
||||||
|
options.add_argument('--disable-search-engine-choice-screen')
|
||||||
# todo: this param shows a warning in chrome head-full
|
# todo: this param shows a warning in chrome head-full
|
||||||
options.add_argument('--disable-setuid-sandbox')
|
options.add_argument('--disable-setuid-sandbox')
|
||||||
options.add_argument('--disable-dev-shm-usage')
|
options.add_argument('--disable-dev-shm-usage')
|
||||||
# this option removes the zygote sandbox (it seems that the resolution is a bit faster)
|
# this option removes the zygote sandbox (it seems that the resolution is a bit faster)
|
||||||
options.add_argument('--no-zygote')
|
options.add_argument('--no-zygote')
|
||||||
# attempt to fix Docker ARM32 build
|
# attempt to fix Docker ARM32 build
|
||||||
options.add_argument('--disable-gpu-sandbox')
|
IS_ARMARCH = platform.machine().startswith(('arm', 'aarch'))
|
||||||
options.add_argument('--disable-software-rasterizer')
|
if IS_ARMARCH:
|
||||||
|
options.add_argument('--disable-gpu-sandbox')
|
||||||
options.add_argument('--ignore-certificate-errors')
|
options.add_argument('--ignore-certificate-errors')
|
||||||
options.add_argument('--ignore-ssl-errors')
|
options.add_argument('--ignore-ssl-errors')
|
||||||
# fix GL errors in ASUSTOR NAS
|
|
||||||
# https://github.com/FlareSolverr/FlareSolverr/issues/782
|
|
||||||
# https://github.com/microsoft/vscode/issues/127800#issuecomment-873342069
|
|
||||||
# https://peter.sh/experiments/chromium-command-line-switches/#use-gl
|
|
||||||
options.add_argument('--use-gl=swiftshader')
|
|
||||||
|
|
||||||
language = os.environ.get('LANG', None)
|
language = os.environ.get('LANG', None)
|
||||||
if language is not None:
|
if language is not None:
|
||||||
@@ -156,6 +161,7 @@ def get_webdriver(proxy: dict = None) -> WebDriver:
|
|||||||
proxy_extension_dir = None
|
proxy_extension_dir = None
|
||||||
if proxy and all(key in proxy for key in ['url', 'username', 'password']):
|
if proxy and all(key in proxy for key in ['url', 'username', 'password']):
|
||||||
proxy_extension_dir = create_proxy_extension(proxy)
|
proxy_extension_dir = create_proxy_extension(proxy)
|
||||||
|
options.add_argument("--disable-features=DisableLoadExtensionCommandLineSwitch")
|
||||||
options.add_argument("--load-extension=%s" % os.path.abspath(proxy_extension_dir))
|
options.add_argument("--load-extension=%s" % os.path.abspath(proxy_extension_dir))
|
||||||
elif proxy and 'url' in proxy:
|
elif proxy and 'url' in proxy:
|
||||||
proxy_url = proxy['url']
|
proxy_url = proxy['url']
|
||||||
@@ -173,8 +179,6 @@ def get_webdriver(proxy: dict = None) -> WebDriver:
|
|||||||
# For normal headless mode:
|
# For normal headless mode:
|
||||||
# options.add_argument('--headless')
|
# options.add_argument('--headless')
|
||||||
|
|
||||||
options.add_argument("--auto-open-devtools-for-tabs")
|
|
||||||
|
|
||||||
# if we are inside the Docker container, we avoid downloading the driver
|
# if we are inside the Docker container, we avoid downloading the driver
|
||||||
driver_exe_path = None
|
driver_exe_path = None
|
||||||
version_main = None
|
version_main = None
|
||||||
@@ -197,6 +201,8 @@ def get_webdriver(proxy: dict = None) -> WebDriver:
|
|||||||
windows_headless=windows_headless, headless=get_config_headless())
|
windows_headless=windows_headless, headless=get_config_headless())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error("Error starting Chrome: %s" % e)
|
logging.error("Error starting Chrome: %s" % e)
|
||||||
|
# No point in continuing if we cannot retrieve the driver
|
||||||
|
raise e
|
||||||
|
|
||||||
# save the patched driver to avoid re-downloads
|
# save the patched driver to avoid re-downloads
|
||||||
if driver_exe_path is None:
|
if driver_exe_path is None:
|
||||||
@@ -298,7 +304,7 @@ def extract_version_nt_folder() -> str:
|
|||||||
paths = [f.path for f in os.scandir(path) if f.is_dir()]
|
paths = [f.path for f in os.scandir(path) if f.is_dir()]
|
||||||
for path in paths:
|
for path in paths:
|
||||||
filename = os.path.basename(path)
|
filename = os.path.basename(path)
|
||||||
pattern = '\d+\.\d+\.\d+\.\d+'
|
pattern = r'\d+\.\d+\.\d+\.\d+'
|
||||||
match = re.search(pattern, filename)
|
match = re.search(pattern, filename)
|
||||||
if match and match.group():
|
if match and match.group():
|
||||||
# Found a Chrome version.
|
# Found a Chrome version.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
WebTest==3.0.0
|
WebTest==3.0.7
|
||||||
|
|||||||
Reference in New Issue
Block a user