mirror of
https://github.com/FlareSolverr/FlareSolverr.git
synced 2025-12-05 17:18:19 +01:00
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
eb680efc90 | ||
|
|
0f8f0bec25 | ||
|
|
3d9bc5627b | ||
|
|
dd7eaee2e3 | ||
|
|
031177bbdb | ||
|
|
a8644532a1 | ||
|
|
e96161c873 | ||
|
|
5a1f25cd52 | ||
|
|
a2c0e4348e | ||
|
|
2ecf88895b | ||
|
|
984368edb5 | ||
|
|
6c1d78cb84 | ||
|
|
5a2c61601e | ||
|
|
c304da2964 | ||
|
|
b811412699 | ||
|
|
0bb8de144f | ||
|
|
38166dfaa0 | ||
|
|
8dea0ed017 | ||
|
|
20cd2944a7 | ||
|
|
fd773e5909 | ||
|
|
35c7bff3c8 | ||
|
|
afdc1c7a8e | ||
|
|
0bc7a4498c | ||
|
|
c5a5f6d65e | ||
|
|
aaf29be8e1 | ||
|
|
800866d033 | ||
|
|
043f18b231 | ||
|
|
d21a332519 | ||
|
|
3ca6d08f41 | ||
|
|
227bd7ac72 | ||
|
|
e6a08584c0 | ||
|
|
df06d13cf8 | ||
|
|
993b8c41ac | ||
|
|
a4d42d7834 | ||
|
|
1c855b8af0 | ||
|
|
745c69491f | ||
|
|
f7e316fd5a | ||
|
|
16c8ab5f3d | ||
|
|
7af311b73c | ||
|
|
daec97532d | ||
|
|
8d7ed48f21 | ||
|
|
220f2599ae | ||
|
|
d772cf3f50 | ||
|
|
ab4365894b | ||
|
|
3fa9631559 | ||
|
|
04858c22fd | ||
|
|
5085ca6990 | ||
|
|
cd4df1e061 |
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 }}"
|
||||||
|
|||||||
72
.github/workflows/release-docker.yml
vendored
72
.github/workflows/release-docker.yml
vendored
@@ -1,53 +1,67 @@
|
|||||||
name: release-docker
|
name: Docker release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- "v*.*.*"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-docker-images:
|
build-docker-images:
|
||||||
runs-on: ubuntu-22.04
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Checkout repository
|
||||||
name: Checkout
|
uses: actions/checkout@v5
|
||||||
uses: actions/checkout@v3
|
|
||||||
-
|
- 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
|
||||||
-
|
|
||||||
name: Docker meta
|
- name: Docker meta
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: crazy-max/ghaction-docker-meta@v3
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REPOSITORY }},ghcr.io/${{ env.REPOSITORY }}
|
images: |
|
||||||
tag-sha: false
|
${{ env.REPOSITORY }},enable=${{ github.event_name != 'pull_request' }}
|
||||||
-
|
ghcr.io/${{ env.REPOSITORY }}
|
||||||
name: Set up QEMU
|
tags: |
|
||||||
uses: docker/setup-qemu-action@v2
|
type=semver,pattern={{version}},prefix=v
|
||||||
-
|
type=ref,event=pr
|
||||||
name: Set up Docker Buildx
|
flavor: |
|
||||||
uses: docker/setup-buildx-action@v2
|
latest=auto
|
||||||
-
|
|
||||||
name: Login to DockerHub
|
- name: Set up QEMU
|
||||||
uses: docker/login-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
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 }}
|
||||||
-
|
|
||||||
name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GH_PAT }}
|
password: ${{ secrets.GH_PAT }}
|
||||||
-
|
|
||||||
name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8
|
platforms: linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: true
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||||
|
|||||||
83
.github/workflows/release.yml
vendored
83
.github/workflows/release.yml
vendored
@@ -1,19 +1,19 @@
|
|||||||
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
|
||||||
@@ -22,74 +22,45 @@ jobs:
|
|||||||
changelog="${changelog//'%'/'%25'}"
|
changelog="${changelog//'%'/'%25'}"
|
||||||
changelog="${changelog//$'\n'/'%0A'}"
|
changelog="${changelog//$'\n'/'%0A'}"
|
||||||
changelog="${changelog//$'\r'/'%0D'}"
|
changelog="${changelog//$'\r'/'%0D'}"
|
||||||
echo "##[set-output name=changelog;]${changelog}"
|
echo "changelog=${changelog}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- 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 }}
|
tag_name: ${{ github.ref }}
|
||||||
release_name: ${{ github.ref }}
|
name: ${{ github.ref }}
|
||||||
body: ${{ steps.github_changelog.outputs.changelog }}
|
body: ${{ env.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_*"]'
|
|
||||||
|
|||||||
114
CHANGELOG.md
114
CHANGELOG.md
@@ -1,6 +1,118 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## v3.3.5 (2023/09/15)
|
## 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)
|
||||||
|
* Add challenge selector to catch reloading page on non-English systems
|
||||||
|
* Escape values for generated form used in request.post. Thanks @mynameisbogdan
|
||||||
|
|
||||||
|
## v3.3.20 (2024/06/21)
|
||||||
|
* maxTimeout should always be int
|
||||||
|
* Check not running in Docker before logging version_main error
|
||||||
|
* Update Cloudflare challenge and checkbox selectors. Thanks @tenettow & @21hsmw
|
||||||
|
|
||||||
|
## v3.3.19 (2024/05/23)
|
||||||
|
* Fix occasional headless issue on Linux when set to "false". Thanks @21hsmw
|
||||||
|
|
||||||
|
## v3.3.18 (2024/05/20)
|
||||||
|
|
||||||
|
* Fix LANG ENV for Linux
|
||||||
|
* Fix Chrome v124+ not closing on Windows. Thanks @RileyXX
|
||||||
|
|
||||||
|
## v3.3.17 (2024/04/09)
|
||||||
|
|
||||||
|
* Fix file descriptor leak in service on quit(). Thanks @zkulis
|
||||||
|
|
||||||
|
## v3.3.16 (2024/02/28)
|
||||||
|
|
||||||
|
* Fix of the subprocess.STARTUPINFO() call. Thanks @ceconelo
|
||||||
|
* Add FreeBSD support. Thanks @Asthowen
|
||||||
|
* Use headless configuration properly. Thanks @hashworks
|
||||||
|
|
||||||
|
## v3.3.15 (2024/02/20)
|
||||||
|
|
||||||
|
* Fix looping challenges
|
||||||
|
|
||||||
|
## v3.3.14-hotfix2 (2024/02/17)
|
||||||
|
|
||||||
|
* Hotfix 2 - bad Chromium build, instances failed to terminate
|
||||||
|
|
||||||
|
## v3.3.14-hotfix (2024/02/17)
|
||||||
|
|
||||||
|
* Hotfix for Linux build - some Chrome files no longer exist
|
||||||
|
|
||||||
|
## v3.3.14 (2024/02/17)
|
||||||
|
|
||||||
|
* Update Chrome downloads. Thanks @opemvbs
|
||||||
|
|
||||||
|
## v3.3.13 (2024/01/07)
|
||||||
|
|
||||||
|
* Fix too many open files error
|
||||||
|
|
||||||
|
## v3.3.12 (2023/12/15)
|
||||||
|
|
||||||
|
* Fix looping challenges and invalid cookies
|
||||||
|
|
||||||
|
## v3.3.11 (2023/12/11)
|
||||||
|
|
||||||
|
* Update UC 3.5.4 & Selenium 4.15.2. Thanks @txtsd
|
||||||
|
|
||||||
|
## v3.3.10 (2023/11/14)
|
||||||
|
|
||||||
|
* Add LANG ENV - resolves issues with YGGtorrent
|
||||||
|
|
||||||
|
## v3.3.9 (2023/11/13)
|
||||||
|
|
||||||
|
* Fix for Docker build, capture TypeError
|
||||||
|
|
||||||
|
## v3.3.8 (2023/11/13)
|
||||||
|
|
||||||
|
* Fix headless=true for Chrome 117+. Thanks @NabiKAZ
|
||||||
|
* Support running Chrome 119 from source. Thanks @koleg and @Chris7X
|
||||||
|
* Fix "OSError: [WinError 6] The handle is invalid" on exit. Thanks @enesgorkemgenc
|
||||||
|
|
||||||
|
## v3.3.7 (2023/11/05)
|
||||||
|
|
||||||
|
* Bump to rebuild. Thanks @JoachimDorchies
|
||||||
|
|
||||||
|
## v3.3.6 (2023/09/15)
|
||||||
|
|
||||||
* Update checkbox selector, again
|
* Update checkbox selector, again
|
||||||
|
|
||||||
|
|||||||
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.6 .
|
# docker build -t ngosang/flaresolverr:3.4.3 .
|
||||||
# docker run -p 8191:8191 ngosang/flaresolverr:3.3.6
|
# docker run -p 8191:8191 ngosang/flaresolverr:3.4.3
|
||||||
|
|
||||||
# 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.6 --platform linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8 .
|
# docker buildx build -t ngosang/flaresolverr:3.4.3 --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.6 --platform linux/arm/v7 --load .
|
# docker buildx build -t ngosang/flaresolverr:3.4.3 --platform linux/arm/v7 --load .
|
||||||
# docker run -p 8191:8191 --platform linux/arm/v7 ngosang/flaresolverr:3.3.6
|
# docker run -p 8191:8191 --platform linux/arm/v7 ngosang/flaresolverr:3.4.3
|
||||||
|
|||||||
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
|
||||||
|
|||||||
62
README.md
62
README.md
@@ -45,7 +45,8 @@ Supported architectures are:
|
|||||||
| ARM32 | linux/arm/v7 |
|
| ARM32 | linux/arm/v7 |
|
||||||
| ARM64 | linux/arm64 |
|
| ARM64 | linux/arm64 |
|
||||||
|
|
||||||
We provide a `docker-compose.yml` configuration file. Clone this repository and execute `docker-compose up -d` to start
|
We provide a `docker-compose.yml` configuration file. Clone this repository and execute
|
||||||
|
`docker-compose up -d` _(Compose V1)_ or `docker compose up -d` _(Compose V2)_ to start
|
||||||
the container.
|
the container.
|
||||||
|
|
||||||
If you prefer the `docker cli` execute the following command.
|
If you prefer the `docker cli` execute the following command.
|
||||||
@@ -58,7 +59,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.
|
||||||
|
|
||||||
@@ -76,30 +77,64 @@ This is the recommended way for Windows users.
|
|||||||
> **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 / macOS) 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.
|
||||||
* 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)
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
* Run `python3.13 -m pip install -r requirements.txt` command to install FlareSolverr dependencies.
|
||||||
|
* Run `python3.13 src/flaresolverr.py` command to start FlareSolverr.
|
||||||
|
|
||||||
### Systemd service
|
### 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.
|
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
|
||||||
|
|
||||||
Example 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' \
|
||||||
--data-raw '{
|
--data-raw '{
|
||||||
"cmd": "request.get",
|
"cmd": "request.get",
|
||||||
"url":"http://www.google.com/",
|
"url": "http://www.google.com/",
|
||||||
"maxTimeout": 60000
|
"maxTimeout": 60000
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example Python request:
|
||||||
|
```py
|
||||||
|
import requests
|
||||||
|
|
||||||
|
url = "http://localhost:8191/v1"
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
data = {
|
||||||
|
"cmd": "request.get",
|
||||||
|
"url": "http://www.google.com/",
|
||||||
|
"maxTimeout": 60000
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, json=data)
|
||||||
|
print(response.text)
|
||||||
|
```
|
||||||
|
|
||||||
|
Example PowerShell request:
|
||||||
|
```ps1
|
||||||
|
$body = @{
|
||||||
|
cmd = "request.get"
|
||||||
|
url = "http://www.google.com/"
|
||||||
|
maxTimeout = 60000
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
irm -UseBasicParsing 'http://localhost:8191/v1' -Headers @{"Content-Type"="application/json"} -Method Post -Body $body
|
||||||
|
```
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
#### + `sessions.create`
|
#### + `sessions.create`
|
||||||
@@ -152,7 +187,9 @@ session. When you no longer need to use a session you should make sure to close
|
|||||||
| 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. |
|
||||||
|
|
||||||
> **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.
|
||||||
@@ -229,11 +266,15 @@ This is the same as `request.get` but it takes one more param:
|
|||||||
| 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_FILE | none | Path to capture log to file. Example: `/config/flaresolver.log`. |
|
||||||
| 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. |
|
||||||
|
| PROXY_URL | none | URL for proxy. Will be overwritten by `request` or `sessions` proxy, if used. Example: `http://127.0.0.1:8080`. |
|
||||||
|
| PROXY_USERNAME | none | Username for proxy. Will be overwritten by `request` or `sessions` proxy, if used. Example: `testuser`. |
|
||||||
|
| PROXY_PASSWORD | none | Password for proxy. Will be overwritten by `request` or `sessions` proxy, if used. Example: `testpass`. |
|
||||||
| CAPTCHA_SOLVER | none | Captcha solving method. It is used when a captcha is encountered. See the Captcha Solvers section. |
|
| CAPTCHA_SOLVER | none | Captcha solving method. It is used when a captcha is encountered. See the Captcha Solvers section. |
|
||||||
| TZ | UTC | Timezone used in the logs and the web browser. Example: `TZ=Europe/London`. |
|
| TZ | UTC | Timezone used in the logs and the web browser. Example: `TZ=Europe/London`. |
|
||||||
|
| LANG | none | Language used in the web browser. Example: `LANG=en_GB`. |
|
||||||
| HEADLESS | true | Only for debugging. To run the web browser in headless mode or visible. |
|
| HEADLESS | true | Only for debugging. To run the web browser in headless mode or visible. |
|
||||||
| BROWSER_TIMEOUT | 40000 | If you are experiencing errors/timeouts because your system is slow, you can try to increase this value. Remember to increase the `maxTimeout` parameter too. |
|
|
||||||
| TEST_URL | https://www.google.com | FlareSolverr makes a request on start to make sure the web browser is working. You can change that URL if it is blocked in your country. |
|
| TEST_URL | https://www.google.com | FlareSolverr makes a request on start to make sure the web browser is working. You can change that URL if it is blocked in your country. |
|
||||||
| PORT | 8191 | Listening port. You don't need to change this if you are running on Docker. |
|
| PORT | 8191 | Listening port. You don't need to change this if you are running on Docker. |
|
||||||
| HOST | 0.0.0.0 | Listening interface. You don't need to change this if you are running on Docker. |
|
| HOST | 0.0.0.0 | Listening interface. You don't need to change this if you are running on Docker. |
|
||||||
@@ -242,8 +283,8 @@ This is the same as `request.get` but it takes one more param:
|
|||||||
|
|
||||||
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.
|
* 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 start FlareSolverr 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 start 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
|
||||||
|
|
||||||
@@ -281,8 +322,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.6",
|
"version": "3.4.3",
|
||||||
"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.11.2
|
selenium==4.36.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.31.0
|
requests==2.32.5
|
||||||
certifi==2023.7.22
|
certifi==2025.10.5
|
||||||
websockets==11.0.3
|
websockets==15.0.1
|
||||||
# only required for linux
|
packaging==25.0
|
||||||
xvfbwrapper==0.2.9
|
# Only required for Linux and macOS
|
||||||
# only required for windows
|
xvfbwrapper==0.2.14; platform_system != "Windows"
|
||||||
pefile==2023.2.7
|
# Only required for Windows
|
||||||
|
pefile==2024.8.26; platform_system == "Windows"
|
||||||
|
|||||||
@@ -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 = "1140001" if os.name == 'nt' else '1140000'
|
revision = "1465706" if os.name == 'nt' else '1465706'
|
||||||
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')
|
||||||
@@ -59,8 +59,7 @@ def download_chromium():
|
|||||||
# Give executable permissions for *nix
|
# Give executable permissions for *nix
|
||||||
# file * | grep executable | cut -d: -f1
|
# file * | grep executable | cut -d: -f1
|
||||||
print("Giving executable permissions...")
|
print("Giving executable permissions...")
|
||||||
execs = ['chrome', 'chrome_crashpad_handler', 'chrome_sandbox', 'chrome-wrapper', 'nacl_helper',
|
execs = ['chrome', 'chrome_crashpad_handler', 'chrome_sandbox', 'chrome-wrapper', 'xdg-mime', 'xdg-settings']
|
||||||
'nacl_helper_bootstrap', 'nacl_irt_x86_64.nexe', 'xdg-mime', 'xdg-settings']
|
|
||||||
for exec_file in execs:
|
for exec_file in execs:
|
||||||
exec_path = os.path.join(chrome_path, exec_file)
|
exec_path = os.path.join(chrome_path, exec_file)
|
||||||
os.chmod(exec_path, 0o755)
|
os.chmod(exec_path, 0o755)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class ChallengeResolutionResultT:
|
|||||||
response: str = None
|
response: str = None
|
||||||
cookies: list = None
|
cookies: list = None
|
||||||
userAgent: str = None
|
userAgent: str = None
|
||||||
|
screenshot: str | None = None
|
||||||
|
|
||||||
def __init__(self, _dict):
|
def __init__(self, _dict):
|
||||||
self.__dict__.update(_dict)
|
self.__dict__.update(_dict)
|
||||||
@@ -41,8 +42,10 @@ 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
|
||||||
|
|
||||||
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
|
||||||
@@ -76,6 +87,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# 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')
|
||||||
@@ -93,6 +105,13 @@ if __name__ == "__main__":
|
|||||||
logging.StreamHandler(sys.stdout)
|
logging.StreamHandler(sys.stdout)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
if log_file:
|
||||||
|
log_file = os.path.realpath(log_file)
|
||||||
|
log_path = os.path.dirname(log_file)
|
||||||
|
os.makedirs(log_path, exist_ok=True)
|
||||||
|
|
||||||
|
logging.getLogger().addHandler(logging.FileHandler(log_file))
|
||||||
|
|
||||||
# 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)
|
||||||
@@ -101,6 +120,9 @@ if __name__ == "__main__":
|
|||||||
logging.info(f'FlareSolverr {utils.get_flaresolverr_version()}')
|
logging.info(f'FlareSolverr {utils.get_flaresolverr_version()}')
|
||||||
logging.debug('Debug log enabled')
|
logging.debug('Debug log enabled')
|
||||||
|
|
||||||
|
# Get current OS for global variable
|
||||||
|
utils.get_current_platform()
|
||||||
|
|
||||||
# test browser installation
|
# test browser installation
|
||||||
flaresolverr_service.test_browser_installation()
|
flaresolverr_service.test_browser_installation()
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import platform
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from urllib.parse import unquote
|
from html import escape
|
||||||
|
from urllib.parse import unquote, quote
|
||||||
|
|
||||||
from func_timeout import FunctionTimedOut, func_timeout
|
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
|
||||||
@@ -40,7 +42,7 @@ CHALLENGE_TITLES = [
|
|||||||
]
|
]
|
||||||
CHALLENGE_SELECTORS = [
|
CHALLENGE_SELECTORS = [
|
||||||
# Cloudflare
|
# Cloudflare
|
||||||
'#cf-challenge-running', '.ray_id', '.attack-box', '#cf-please-wait', '#challenge-spinner', '#trk_jschal_js',
|
'#cf-challenge-running', '.ray_id', '.attack-box', '#cf-please-wait', '#challenge-spinner', '#trk_jschal_js', '#turnstile-wrapper', '.lds-ring',
|
||||||
# Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
|
# Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
|
||||||
'td.info #js_info',
|
'td.info #js_info',
|
||||||
# Fairlane / pararius.com
|
# Fairlane / pararius.com
|
||||||
@@ -119,7 +121,7 @@ def _controller_v1_handler(req: V1RequestBase) -> V1ResponseBase:
|
|||||||
logging.warning("Request parameter 'userAgent' was removed in FlareSolverr v2.")
|
logging.warning("Request parameter 'userAgent' was removed in FlareSolverr v2.")
|
||||||
|
|
||||||
# set default values
|
# set default values
|
||||||
if req.maxTimeout is None or req.maxTimeout < 1:
|
if req.maxTimeout is None or int(req.maxTimeout) < 1:
|
||||||
req.maxTimeout = 60000
|
req.maxTimeout = 60000
|
||||||
|
|
||||||
# execute the command
|
# execute the command
|
||||||
@@ -220,7 +222,7 @@ def _cmd_sessions_destroy(req: V1RequestBase) -> V1ResponseBase:
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_challenge(req: V1RequestBase, method: str) -> ChallengeResolutionT:
|
def _resolve_challenge(req: V1RequestBase, method: str) -> ChallengeResolutionT:
|
||||||
timeout = req.maxTimeout / 1000
|
timeout = int(req.maxTimeout) / 1000
|
||||||
driver = None
|
driver = None
|
||||||
try:
|
try:
|
||||||
if req.session:
|
if req.session:
|
||||||
@@ -245,6 +247,8 @@ def _resolve_challenge(req: V1RequestBase, method: str) -> ChallengeResolutionT:
|
|||||||
raise Exception('Error solving the challenge. ' + str(e).replace('\n', '\\n'))
|
raise Exception('Error solving the challenge. ' + str(e).replace('\n', '\\n'))
|
||||||
finally:
|
finally:
|
||||||
if not req.session and driver is not None:
|
if not req.session and driver is not None:
|
||||||
|
if utils.PLATFORM_VERSION == "nt":
|
||||||
|
driver.close()
|
||||||
driver.quit()
|
driver.quit()
|
||||||
logging.debug('A used instance of webdriver has been destroyed')
|
logging.debug('A used instance of webdriver has been destroyed')
|
||||||
|
|
||||||
@@ -252,18 +256,9 @@ def _resolve_challenge(req: V1RequestBase, method: str) -> ChallengeResolutionT:
|
|||||||
def click_verify(driver: WebDriver):
|
def click_verify(driver: WebDriver):
|
||||||
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).send_keys(Keys.TAB).pause(1).send_keys(Keys.SPACE).perform()
|
||||||
checkbox = driver.find_element(
|
logging.debug("Cloudflare verify checkbox found and clicked!")
|
||||||
by=By.XPATH,
|
|
||||||
value='//*[@id="challenge-stage"]/div/label/input',
|
|
||||||
)
|
|
||||||
if checkbox:
|
|
||||||
actions = ActionChains(driver)
|
|
||||||
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:
|
||||||
@@ -292,13 +287,13 @@ def _evil_logic(req: V1RequestBase, driver: WebDriver, method: str) -> Challenge
|
|||||||
res.status = STATUS_OK
|
res.status = STATUS_OK
|
||||||
res.message = ""
|
res.message = ""
|
||||||
|
|
||||||
|
|
||||||
# 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':
|
if method == 'POST':
|
||||||
_post_request(req, driver)
|
_post_request(req, driver)
|
||||||
else:
|
else:
|
||||||
driver.get(req.url)
|
driver.get(req.url)
|
||||||
driver.start_session() # required to bypass Cloudflare
|
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -311,7 +306,6 @@ def _evil_logic(req: V1RequestBase, driver: WebDriver, method: str) -> Challenge
|
|||||||
_post_request(req, driver)
|
_post_request(req, driver)
|
||||||
else:
|
else:
|
||||||
driver.get(req.url)
|
driver.get(req.url)
|
||||||
driver.start_session() # required to bypass Cloudflare
|
|
||||||
|
|
||||||
# wait for the page
|
# wait for the page
|
||||||
if utils.get_config_log_html():
|
if utils.get_config_log_html():
|
||||||
@@ -321,7 +315,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
|
||||||
@@ -396,18 +390,26 @@ def _evil_logic(req: V1RequestBase, driver: WebDriver, method: str) -> Challenge
|
|||||||
|
|
||||||
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])
|
||||||
@@ -417,10 +419,12 @@ 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 ''
|
||||||
post_form += f'<input type="text" name="{name}" value="{value}"><br>'
|
# 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 += '</form>'
|
post_form += '</form>'
|
||||||
html_content = f"""
|
html_content = f"""
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -430,5 +434,4 @@ def _post_request(req: V1RequestBase, driver: WebDriver):
|
|||||||
<script>document.getElementById('hackForm').submit();</script>
|
<script>document.getElementById('hackForm').submit();</script>
|
||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>"""
|
||||||
driver.get("data:text/html;charset=utf-8," + html_content)
|
driver.get("data:text/html;charset=utf-8,{html_content}".format(html_content=html_content))
|
||||||
driver.start_session() # required to bypass Cloudflare
|
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ class SessionsStorage:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
session = self.sessions.pop(session_id)
|
session = self.sessions.pop(session_id)
|
||||||
|
if utils.PLATFORM_VERSION == "nt":
|
||||||
|
session.driver.close()
|
||||||
session.driver.quit()
|
session.driver.quit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ by UltrafunkAmsterdam (https://github.com/ultrafunkamsterdam)
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
__version__ = "3.5.3"
|
__version__ = "3.5.5"
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -396,7 +396,7 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
|
|||||||
if no_sandbox:
|
if no_sandbox:
|
||||||
options.arguments.extend(["--no-sandbox", "--test-type"])
|
options.arguments.extend(["--no-sandbox", "--test-type"])
|
||||||
|
|
||||||
if headless or options.headless:
|
if headless or getattr(options, 'headless', None):
|
||||||
#workaround until a better checking is found
|
#workaround until a better checking is found
|
||||||
try:
|
try:
|
||||||
v_main = int(self.patcher.version_main) if self.patcher.version_main else 108
|
v_main = int(self.patcher.version_main) if self.patcher.version_main else 108
|
||||||
@@ -451,8 +451,10 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
|
|||||||
options.binary_location, *options.arguments
|
options.binary_location, *options.arguments
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
startupinfo = subprocess.STARTUPINFO()
|
startupinfo = None
|
||||||
if os.name == 'nt' and windows_headless:
|
if os.name == 'nt' and windows_headless:
|
||||||
|
# STARTUPINFO() is Windows only
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
browser = subprocess.Popen(
|
browser = subprocess.Popen(
|
||||||
[options.binary_location, *options.arguments],
|
[options.binary_location, *options.arguments],
|
||||||
@@ -464,14 +466,12 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
|
|||||||
)
|
)
|
||||||
self.browser_pid = browser.pid
|
self.browser_pid = browser.pid
|
||||||
|
|
||||||
# Fix for Chrome 115
|
|
||||||
# https://github.com/seleniumbase/SeleniumBase/pull/1967
|
|
||||||
service = selenium.webdriver.chromium.service.ChromiumService(
|
service = selenium.webdriver.chromium.service.ChromiumService(
|
||||||
executable_path=self.patcher.executable_path,
|
self.patcher.executable_path
|
||||||
service_args=["--disable-build-check"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
super(Chrome, self).__init__(
|
super().__init__(
|
||||||
service=service,
|
service=service,
|
||||||
options=options,
|
options=options,
|
||||||
keep_alive=keep_alive,
|
keep_alive=keep_alive,
|
||||||
@@ -493,7 +493,7 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
|
|||||||
else:
|
else:
|
||||||
self._web_element_cls = WebElement
|
self._web_element_cls = WebElement
|
||||||
|
|
||||||
if options.headless:
|
if headless or getattr(options, 'headless', None):
|
||||||
self._configure_headless()
|
self._configure_headless()
|
||||||
|
|
||||||
def _configure_headless(self):
|
def _configure_headless(self):
|
||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
@@ -771,7 +767,9 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
|
|||||||
|
|
||||||
def quit(self):
|
def quit(self):
|
||||||
try:
|
try:
|
||||||
|
self.service.stop()
|
||||||
self.service.process.kill()
|
self.service.process.kill()
|
||||||
|
self.command_executor.close()
|
||||||
self.service.process.wait(5)
|
self.service.process.wait(5)
|
||||||
logger.debug("webdriver process ended")
|
logger.debug("webdriver process ended")
|
||||||
except (AttributeError, RuntimeError, OSError):
|
except (AttributeError, RuntimeError, OSError):
|
||||||
@@ -786,15 +784,6 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
|
|||||||
logger.debug("gracefully closed browser")
|
logger.debug("gracefully closed browser")
|
||||||
except Exception as e: # noqa
|
except Exception as e: # noqa
|
||||||
pass
|
pass
|
||||||
# Force kill Chrome process in Windows
|
|
||||||
# https://github.com/FlareSolverr/FlareSolverr/issues/772
|
|
||||||
if os.name == 'nt':
|
|
||||||
try:
|
|
||||||
subprocess.call(['taskkill', '/f', '/pid', str(self.browser_pid)],
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if (
|
if (
|
||||||
hasattr(self, "keep_user_data_dir")
|
hasattr(self, "keep_user_data_dir")
|
||||||
and hasattr(self, "user_data_dir")
|
and hasattr(self, "user_data_dir")
|
||||||
@@ -813,7 +802,11 @@ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
|
|||||||
else:
|
else:
|
||||||
logger.debug("successfully removed %s" % self.user_data_dir)
|
logger.debug("successfully removed %s" % self.user_data_dir)
|
||||||
break
|
break
|
||||||
time.sleep(0.1)
|
|
||||||
|
try:
|
||||||
|
time.sleep(0.1)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
# dereference patcher, so patcher can start cleaning up as well.
|
# dereference patcher, so patcher can start cleaning up as well.
|
||||||
# this must come last, otherwise it will throw 'in use' errors
|
# this must come last, otherwise it will throw 'in use' errors
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
import os
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -187,4 +188,6 @@ def test():
|
|||||||
|
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
driver.close()
|
||||||
driver.quit()
|
driver.quit()
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ def start_detached(executable, *args):
|
|||||||
# close pipes
|
# close pipes
|
||||||
writer.close()
|
writer.close()
|
||||||
reader.close()
|
reader.close()
|
||||||
|
process.close()
|
||||||
|
|
||||||
return pid
|
return pid
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -21,7 +22,7 @@ from multiprocessing import Lock
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
IS_POSIX = sys.platform.startswith(("darwin", "cygwin", "linux", "linux2"))
|
IS_POSIX = sys.platform.startswith(("darwin", "cygwin", "linux", "linux2", "freebsd"))
|
||||||
|
|
||||||
|
|
||||||
class Patcher(object):
|
class Patcher(object):
|
||||||
@@ -62,7 +63,19 @@ class Patcher(object):
|
|||||||
prefix = "undetected"
|
prefix = "undetected"
|
||||||
self.user_multi_procs = user_multi_procs
|
self.user_multi_procs = user_multi_procs
|
||||||
|
|
||||||
self.is_old_chromedriver = version_main and version_main <= 114
|
try:
|
||||||
|
# Try to convert version_main into an integer
|
||||||
|
version_main_int = int(version_main)
|
||||||
|
# check if version_main_int is less than or equal to e.g 114
|
||||||
|
self.is_old_chromedriver = version_main and version_main_int <= 114
|
||||||
|
except (ValueError,TypeError):
|
||||||
|
# Check not running inside Docker
|
||||||
|
if not os.path.exists("/app/chromedriver"):
|
||||||
|
# If the conversion fails, log an error message
|
||||||
|
logging.info("version_main cannot be converted to an integer")
|
||||||
|
# Set self.is_old_chromedriver to False if the conversion fails
|
||||||
|
self.is_old_chromedriver = False
|
||||||
|
|
||||||
# Needs to be called before self.exe_name is accessed
|
# Needs to be called before self.exe_name is accessed
|
||||||
self._set_platform_name()
|
self._set_platform_name()
|
||||||
|
|
||||||
@@ -70,9 +83,14 @@ class Patcher(object):
|
|||||||
os.makedirs(self.data_path, exist_ok=True)
|
os.makedirs(self.data_path, exist_ok=True)
|
||||||
|
|
||||||
if not executable_path:
|
if not executable_path:
|
||||||
self.executable_path = os.path.join(
|
if sys.platform.startswith("freebsd"):
|
||||||
self.data_path, "_".join([prefix, self.exe_name])
|
self.executable_path = os.path.join(
|
||||||
)
|
self.data_path, self.exe_name
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.executable_path = os.path.join(
|
||||||
|
self.data_path, "_".join([prefix, self.exe_name])
|
||||||
|
)
|
||||||
|
|
||||||
if not IS_POSIX:
|
if not IS_POSIX:
|
||||||
if executable_path:
|
if executable_path:
|
||||||
@@ -117,6 +135,9 @@ class Patcher(object):
|
|||||||
else:
|
else:
|
||||||
self.platform_name = "mac-x64"
|
self.platform_name = "mac-x64"
|
||||||
self.exe_name %= ""
|
self.exe_name %= ""
|
||||||
|
if self.platform.startswith("freebsd"):
|
||||||
|
self.platform_name = "freebsd"
|
||||||
|
self.exe_name %= ""
|
||||||
|
|
||||||
def auto(self, executable_path=None, force=False, version_main=None, _=None):
|
def auto(self, executable_path=None, force=False, version_main=None, _=None):
|
||||||
"""
|
"""
|
||||||
@@ -156,26 +177,56 @@ class Patcher(object):
|
|||||||
if force is True:
|
if force is True:
|
||||||
self.force = force
|
self.force = force
|
||||||
|
|
||||||
try:
|
|
||||||
os.unlink(self.executable_path)
|
|
||||||
except PermissionError:
|
|
||||||
if self.force:
|
|
||||||
self.force_kill_instances(self.executable_path)
|
|
||||||
return self.auto(force=not self.force)
|
|
||||||
try:
|
|
||||||
if self.is_binary_patched():
|
|
||||||
# assumes already running AND patched
|
|
||||||
return True
|
|
||||||
except PermissionError:
|
|
||||||
pass
|
|
||||||
# return False
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
release = self.fetch_release_number()
|
if self.platform_name == "freebsd":
|
||||||
self.version_main = release.version[0]
|
chromedriver_path = shutil.which("chromedriver")
|
||||||
self.version_full = release
|
|
||||||
self.unzip_package(self.fetch_package())
|
if not os.path.isfile(chromedriver_path) or not os.access(chromedriver_path, os.X_OK):
|
||||||
|
logging.error("Chromedriver not installed!")
|
||||||
|
return
|
||||||
|
|
||||||
|
version_path = os.path.join(os.path.dirname(self.executable_path), "version.txt")
|
||||||
|
|
||||||
|
process = os.popen(f'"{chromedriver_path}" --version')
|
||||||
|
chromedriver_version = process.read().split(' ')[1].split(' ')[0]
|
||||||
|
process.close()
|
||||||
|
|
||||||
|
current_version = None
|
||||||
|
if os.path.isfile(version_path) or os.access(version_path, os.X_OK):
|
||||||
|
with open(version_path, 'r') as f:
|
||||||
|
current_version = f.read()
|
||||||
|
|
||||||
|
if current_version != chromedriver_version:
|
||||||
|
logging.info("Copying chromedriver executable...")
|
||||||
|
shutil.copy(chromedriver_path, self.executable_path)
|
||||||
|
os.chmod(self.executable_path, 0o755)
|
||||||
|
|
||||||
|
with open(version_path, 'w') as f:
|
||||||
|
f.write(chromedriver_version)
|
||||||
|
|
||||||
|
logging.info("Chromedriver executable copied!")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
os.unlink(self.executable_path)
|
||||||
|
except PermissionError:
|
||||||
|
if self.force:
|
||||||
|
self.force_kill_instances(self.executable_path)
|
||||||
|
return self.auto(force=not self.force)
|
||||||
|
try:
|
||||||
|
if self.is_binary_patched():
|
||||||
|
# assumes already running AND patched
|
||||||
|
return True
|
||||||
|
except PermissionError:
|
||||||
|
pass
|
||||||
|
# return False
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
release = self.fetch_release_number()
|
||||||
|
self.version_main = release.major
|
||||||
|
self.version_full = release
|
||||||
|
self.unzip_package(self.fetch_package())
|
||||||
|
|
||||||
return self.patch()
|
return self.patch()
|
||||||
|
|
||||||
def driver_binary_in_use(self, path: str = None) -> bool:
|
def driver_binary_in_use(self, path: str = None) -> bool:
|
||||||
@@ -277,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://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/%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]
|
||||||
@@ -323,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():
|
||||||
|
|||||||
72
src/utils.py
72
src/utils.py
@@ -1,15 +1,18 @@
|
|||||||
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 sys
|
||||||
import tempfile
|
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
|
||||||
|
|
||||||
FLARESOLVERR_VERSION = None
|
FLARESOLVERR_VERSION = None
|
||||||
|
PLATFORM_VERSION = None
|
||||||
CHROME_EXE_PATH = None
|
CHROME_EXE_PATH = None
|
||||||
CHROME_MAJOR_VERSION = None
|
CHROME_MAJOR_VERSION = None
|
||||||
USER_AGENT = None
|
USER_AGENT = None
|
||||||
@@ -37,6 +40,13 @@ def get_flaresolverr_version() -> str:
|
|||||||
FLARESOLVERR_VERSION = json.loads(f.read())['version']
|
FLARESOLVERR_VERSION = json.loads(f.read())['version']
|
||||||
return FLARESOLVERR_VERSION
|
return FLARESOLVERR_VERSION
|
||||||
|
|
||||||
|
def get_current_platform() -> str:
|
||||||
|
global PLATFORM_VERSION
|
||||||
|
if PLATFORM_VERSION is not None:
|
||||||
|
return PLATFORM_VERSION
|
||||||
|
PLATFORM_VERSION = os.name
|
||||||
|
return PLATFORM_VERSION
|
||||||
|
|
||||||
|
|
||||||
def create_proxy_extension(proxy: dict) -> str:
|
def create_proxy_extension(proxy: dict) -> str:
|
||||||
parsed_url = urllib.parse.urlparse(proxy['url'])
|
parsed_url = urllib.parse.urlparse(proxy['url'])
|
||||||
@@ -48,18 +58,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"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -113,39 +126,45 @@ def create_proxy_extension(proxy: dict) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def get_webdriver(proxy: dict = None) -> WebDriver:
|
def get_webdriver(proxy: dict = None) -> WebDriver:
|
||||||
global PATCHED_DRIVER_PATH
|
global PATCHED_DRIVER_PATH, USER_AGENT
|
||||||
logging.debug('Launching web browser...')
|
logging.debug('Launching web browser...')
|
||||||
|
|
||||||
# undetected_chromedriver
|
# undetected_chromedriver
|
||||||
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
|
language = os.environ.get('LANG', None)
|
||||||
# https://github.com/microsoft/vscode/issues/127800#issuecomment-873342069
|
if language is not None:
|
||||||
# https://peter.sh/experiments/chromium-command-line-switches/#use-gl
|
options.add_argument('--accept-lang=%s' % language)
|
||||||
options.add_argument('--use-gl=swiftshader')
|
|
||||||
|
# Fix for Chrome 117 | https://github.com/FlareSolverr/FlareSolverr/issues/910
|
||||||
|
if USER_AGENT is not None:
|
||||||
|
options.add_argument('--user-agent=%s' % USER_AGENT)
|
||||||
|
|
||||||
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']
|
||||||
logging.debug("Using webdriver proxy: %s", proxy_url)
|
logging.debug("Using webdriver proxy: %s", proxy_url)
|
||||||
options.add_argument('--proxy-server=%s' % proxy_url)
|
options.add_argument('--proxy-server=%s' % proxy_url)
|
||||||
|
|
||||||
# note: headless mode is detected (options.headless = True)
|
# note: headless mode is detected (headless = True)
|
||||||
# we launch the browser in head-full mode with the window hidden
|
# we launch the browser in head-full mode with the window hidden
|
||||||
windows_headless = False
|
windows_headless = False
|
||||||
if get_config_headless():
|
if get_config_headless():
|
||||||
@@ -153,6 +172,8 @@ def get_webdriver(proxy: dict = None) -> WebDriver:
|
|||||||
windows_headless = True
|
windows_headless = True
|
||||||
else:
|
else:
|
||||||
start_xvfb_display()
|
start_xvfb_display()
|
||||||
|
# For normal headless mode:
|
||||||
|
# options.add_argument('--headless')
|
||||||
|
|
||||||
# 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
|
||||||
@@ -162,10 +183,6 @@ def get_webdriver(proxy: dict = None) -> WebDriver:
|
|||||||
driver_exe_path = "/app/chromedriver"
|
driver_exe_path = "/app/chromedriver"
|
||||||
else:
|
else:
|
||||||
version_main = get_chrome_major_version()
|
version_main = get_chrome_major_version()
|
||||||
# Fix for Chrome 115
|
|
||||||
# https://github.com/seleniumbase/SeleniumBase/pull/1967
|
|
||||||
if int(version_main) > 114:
|
|
||||||
version_main = 114
|
|
||||||
if PATCHED_DRIVER_PATH is not None:
|
if PATCHED_DRIVER_PATH is not None:
|
||||||
driver_exe_path = PATCHED_DRIVER_PATH
|
driver_exe_path = PATCHED_DRIVER_PATH
|
||||||
|
|
||||||
@@ -174,9 +191,14 @@ def get_webdriver(proxy: dict = None) -> WebDriver:
|
|||||||
|
|
||||||
# downloads and patches the chromedriver
|
# downloads and patches the chromedriver
|
||||||
# if we don't set driver_executable_path it downloads, patches, and deletes the driver each time
|
# if we don't set driver_executable_path it downloads, patches, and deletes the driver each time
|
||||||
driver = uc.Chrome(options=options, browser_executable_path=browser_executable_path,
|
try:
|
||||||
driver_executable_path=driver_exe_path, version_main=version_main,
|
driver = uc.Chrome(options=options, browser_executable_path=browser_executable_path,
|
||||||
windows_headless=windows_headless, headless=windows_headless)
|
driver_executable_path=driver_exe_path, version_main=version_main,
|
||||||
|
windows_headless=windows_headless, headless=get_config_headless())
|
||||||
|
except Exception as 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:
|
||||||
@@ -278,7 +300,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.
|
||||||
@@ -295,11 +317,15 @@ def get_user_agent(driver=None) -> str:
|
|||||||
if driver is None:
|
if driver is None:
|
||||||
driver = get_webdriver()
|
driver = get_webdriver()
|
||||||
USER_AGENT = driver.execute_script("return navigator.userAgent")
|
USER_AGENT = driver.execute_script("return navigator.userAgent")
|
||||||
|
# Fix for Chrome 117 | https://github.com/FlareSolverr/FlareSolverr/issues/910
|
||||||
|
USER_AGENT = re.sub('HEADLESS', '', USER_AGENT, flags=re.IGNORECASE)
|
||||||
return USER_AGENT
|
return USER_AGENT
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception("Error getting browser User-Agent. " + str(e))
|
raise Exception("Error getting browser User-Agent. " + str(e))
|
||||||
finally:
|
finally:
|
||||||
if driver is not None:
|
if driver is not None:
|
||||||
|
if PLATFORM_VERSION == "nt":
|
||||||
|
driver.close()
|
||||||
driver.quit()
|
driver.quit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
WebTest==3.0.0
|
WebTest==3.0.6
|
||||||
|
|||||||
Reference in New Issue
Block a user