overwrite69 commited on
Commit
813af16
·
verified ·
1 Parent(s): 22969c5

Upload 44 files

Browse files
Files changed (44) hide show
  1. .dockerignore +5 -0
  2. .github/ISSUE_TEMPLATE/bug_report.yml +78 -0
  3. .github/ISSUE_TEMPLATE/config.yml +8 -0
  4. .github/workflows/autotag.yml +19 -0
  5. .github/workflows/release-docker.yml +67 -0
  6. .github/workflows/release.yml +63 -0
  7. .gitignore +129 -0
  8. CHANGELOG.md +470 -0
  9. Dockerfile +63 -0
  10. LICENSE +21 -0
  11. README.md +347 -10
  12. docker-compose.yml +18 -0
  13. flaresolverr.service +19 -0
  14. html_samples/cloudflare_captcha_hcaptcha_v1.html +219 -0
  15. html_samples/cloudflare_captcha_norobot_v1.html +170 -0
  16. html_samples/cloudflare_init_v1.html +120 -0
  17. html_samples/cloudflare_spinner_v1.html +167 -0
  18. package.json +7 -0
  19. requirements.txt +14 -0
  20. resources/flaresolverr_logo.ico +0 -0
  21. resources/flaresolverr_logo.png +0 -0
  22. resources/flaresolverr_logo.svg +180 -0
  23. src/bottle_plugins/__init__.py +0 -0
  24. src/bottle_plugins/error_plugin.py +22 -0
  25. src/bottle_plugins/logger_plugin.py +23 -0
  26. src/bottle_plugins/prometheus_plugin.py +66 -0
  27. src/build_package.py +126 -0
  28. src/dtos.py +94 -0
  29. src/flaresolverr.py +152 -0
  30. src/flaresolverr_service.py +519 -0
  31. src/metrics.py +32 -0
  32. src/sessions.py +84 -0
  33. src/tests.py +655 -0
  34. src/tests_sites.py +102 -0
  35. src/undetected_chromedriver/__init__.py +910 -0
  36. src/undetected_chromedriver/cdp.py +112 -0
  37. src/undetected_chromedriver/devtool.py +193 -0
  38. src/undetected_chromedriver/dprocess.py +77 -0
  39. src/undetected_chromedriver/options.py +85 -0
  40. src/undetected_chromedriver/patcher.py +473 -0
  41. src/undetected_chromedriver/reactor.py +99 -0
  42. src/undetected_chromedriver/webelement.py +86 -0
  43. src/utils.py +347 -0
  44. test-requirements.txt +1 -0
.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .git/
2
+ .github/
3
+ .idea/
4
+ html_samples/
5
+ resources/
.github/ISSUE_TEMPLATE/bug_report.yml ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Bug report
2
+ description: Create a report of your issue
3
+ body:
4
+ - type: checkboxes
5
+ attributes:
6
+ label: Have you checked our README?
7
+ description: Please check the <a href="https://github.com/FlareSolverr/FlareSolverr/blob/master/README.md">README</a>.
8
+ options:
9
+ - label: I have checked the README
10
+ required: true
11
+ - type: checkboxes
12
+ attributes:
13
+ label: Have you followed our Troubleshooting?
14
+ description: Please follow our <a href="https://github.com/FlareSolverr/FlareSolverr/wiki/Troubleshooting">Troubleshooting</a>.
15
+ options:
16
+ - label: I have followed your Troubleshooting
17
+ required: true
18
+ - type: checkboxes
19
+ attributes:
20
+ label: Is there already an issue for your problem?
21
+ description: Please make sure you are not creating an already submitted <a href="https://github.com/FlareSolverr/FlareSolverr/issues">Issue</a>. Check closed issues as well, because your issue may have already been fixed.
22
+ options:
23
+ - label: I have checked older issues, open and closed
24
+ required: true
25
+ - type: checkboxes
26
+ attributes:
27
+ label: Have you checked the discussions?
28
+ description: Please read our <a href="https://github.com/FlareSolverr/FlareSolverr/discussions">Discussions</a> before submitting your issue, some wider problems may be dealt with there.
29
+ options:
30
+ - label: I have read the Discussions
31
+ required: true
32
+ - type: input
33
+ attributes:
34
+ label: Have you ACTUALLY checked all these?
35
+ 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.
36
+ placeholder: YES or NO
37
+ validations:
38
+ required: true
39
+ - type: textarea
40
+ attributes:
41
+ label: Environment
42
+ description: Please provide the details of the system FlareSolverr is running on.
43
+ value: |
44
+ - FlareSolverr version:
45
+ - Last working FlareSolverr version:
46
+ - Operating system:
47
+ - Are you using Docker: [yes/no]
48
+ - FlareSolverr User-Agent (see log traces or / endpoint):
49
+ - Are you using a VPN: [yes/no]
50
+ - Are you using a Proxy: [yes/no]
51
+ - Are you using Captcha Solver: [yes/no]
52
+ - If using captcha solver, which one:
53
+ - URL to test this issue:
54
+ render: markdown
55
+ validations:
56
+ required: true
57
+ - type: textarea
58
+ attributes:
59
+ label: Description
60
+ description: List steps to reproduce the error and details on what happens and what you expected to happen.
61
+ validations:
62
+ required: true
63
+ - type: textarea
64
+ attributes:
65
+ label: Logged Error Messages
66
+ description: |
67
+ Place any relevant error messages you noticed from the logs here.
68
+ Make sure you attach the full logs with your personal information removed in case we need more information.
69
+ If you wish to provide debug logs, follow the instructions from this <a href="https://github.com/FlareSolverr/FlareSolverr/wiki/How-to-enable-debug-and-html-trace">wiki page</a>.
70
+ render: text
71
+ validations:
72
+ required: true
73
+ - type: textarea
74
+ attributes:
75
+ label: Screenshots
76
+ description: Place any screenshots of the issue here if needed
77
+ validations:
78
+ required: false
.github/ISSUE_TEMPLATE/config.yml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Requesting new features or changes
4
+ url: https://github.com/FlareSolverr/FlareSolverr/discussions
5
+ about: Please create a new discussion topic, grouped under "Ideas".
6
+ - name: Asking questions
7
+ url: https://github.com/FlareSolverr/FlareSolverr/discussions
8
+ about: Please create a new discussion topic, grouped under "Q&A".
.github/workflows/autotag.yml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Autotag
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "master"
7
+
8
+ jobs:
9
+ tag-release:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout repository
13
+ uses: actions/checkout@v6
14
+
15
+ - name: Auto tag
16
+ uses: Klemensas/action-autotag@stable
17
+ with:
18
+ GITHUB_TOKEN: "${{ secrets.GH_PAT }}"
19
+ tag_prefix: "v"
.github/workflows/release-docker.yml ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Docker release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+ pull_request:
8
+ branches:
9
+ - master
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ build-docker-images:
17
+ if: ${{ !github.event.pull_request.head.repo.fork }}
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - name: Checkout repository
21
+ uses: actions/checkout@v6
22
+
23
+ - name: Downcase repo
24
+ run: echo REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
25
+
26
+ - name: Docker meta
27
+ id: docker_meta
28
+ uses: docker/metadata-action@v5
29
+ with:
30
+ images: |
31
+ ${{ env.REPOSITORY }},enable=${{ github.event_name != 'pull_request' }}
32
+ ghcr.io/${{ env.REPOSITORY }}
33
+ tags: |
34
+ type=semver,pattern={{version}},prefix=v
35
+ type=ref,event=pr
36
+ flavor: |
37
+ latest=auto
38
+
39
+ - name: Set up QEMU
40
+ uses: docker/setup-qemu-action@v3
41
+
42
+ - name: Set up Docker Buildx
43
+ uses: docker/setup-buildx-action@v3
44
+
45
+ - name: Login to DockerHub
46
+ if: github.event_name != 'pull_request'
47
+ uses: docker/login-action@v3
48
+ with:
49
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
50
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
51
+
52
+ - name: Login to GitHub Container Registry
53
+ uses: docker/login-action@v3
54
+ with:
55
+ registry: ghcr.io
56
+ username: ${{ github.repository_owner }}
57
+ password: ${{ secrets.GH_PAT }}
58
+
59
+ - name: Build and push
60
+ uses: docker/build-push-action@v6
61
+ with:
62
+ context: .
63
+ file: ./Dockerfile
64
+ platforms: linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8
65
+ push: true
66
+ tags: ${{ steps.docker_meta.outputs.tags }}
67
+ labels: ${{ steps.docker_meta.outputs.labels }}
.github/workflows/release.yml ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+
8
+ jobs:
9
+ create-release:
10
+ name: Create release
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout repository
14
+ uses: actions/checkout@v6
15
+ with:
16
+ fetch-depth: 0
17
+
18
+ - name: Build changelog
19
+ id: github_changelog
20
+ run: |
21
+ changelog=$(git log $(git tag | tail -2 | head -1)..HEAD --no-merges --oneline)
22
+ echo "changelog<<EOF" >> $GITHUB_OUTPUT
23
+ echo "$changelog" >> $GITHUB_OUTPUT
24
+ echo "EOF" >> $GITHUB_OUTPUT
25
+
26
+ - name: Create release
27
+ uses: softprops/action-gh-release@v2
28
+ with:
29
+ body: ${{ steps.github_changelog.outputs.changelog }}
30
+ env:
31
+ GITHUB_TOKEN: ${{ secrets.GH_PAT }}
32
+
33
+ build-package:
34
+ name: Build binaries
35
+ needs: create-release
36
+ runs-on: ${{ matrix.os }}
37
+ strategy:
38
+ matrix:
39
+ os: [ubuntu-latest, windows-latest]
40
+ steps:
41
+ - name: Checkout repository
42
+ uses: actions/checkout@v6
43
+ with:
44
+ fetch-depth: 0
45
+
46
+ - name: Setup Python
47
+ uses: actions/setup-python@v6
48
+ with:
49
+ python-version: "3.13"
50
+
51
+ - name: Build artifacts
52
+ run: |
53
+ python -m pip install -r requirements.txt
54
+ python -m pip install pyinstaller==6.17.0
55
+ cd src
56
+ python build_package.py
57
+
58
+ - name: Upload release artifacts
59
+ uses: softprops/action-gh-release@v2
60
+ with:
61
+ files: ./dist/flaresolverr_*
62
+ env:
63
+ GITHUB_TOKEN: ${{ secrets.GH_PAT }}
.gitignore ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Editors
2
+ .vscode/
3
+ .idea/
4
+
5
+ # Vagrant
6
+ .vagrant/
7
+
8
+ # Mac/OSX
9
+ .DS_Store
10
+
11
+ # Windows
12
+ Thumbs.db
13
+
14
+ # Source for the following rules: https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore
15
+ # Byte-compiled / optimized / DLL files
16
+ __pycache__/
17
+ *.py[cod]
18
+ *$py.class
19
+
20
+ # C extensions
21
+ *.so
22
+
23
+ # Distribution / packaging
24
+ .Python
25
+ build/
26
+ develop-eggs/
27
+ dist/
28
+ dist_chrome/
29
+ downloads/
30
+ eggs/
31
+ .eggs/
32
+ lib/
33
+ lib64/
34
+ parts/
35
+ sdist/
36
+ var/
37
+ wheels/
38
+ *.egg-info/
39
+ .installed.cfg
40
+ *.egg
41
+ MANIFEST
42
+
43
+ # PyInstaller
44
+ # Usually these files are written by a python script from a template
45
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
46
+ *.manifest
47
+ *.spec
48
+
49
+ # Installer logs
50
+ pip-log.txt
51
+ pip-delete-this-directory.txt
52
+
53
+ # Unit test / coverage reports
54
+ htmlcov/
55
+ .tox/
56
+ .nox/
57
+ .coverage
58
+ .coverage.*
59
+ .cache
60
+ nosetests.xml
61
+ coverage.xml
62
+ *.cover
63
+ .hypothesis/
64
+ .pytest_cache/
65
+
66
+ # Translations
67
+ *.mo
68
+ *.pot
69
+
70
+ # Django stuff:
71
+ *.log
72
+ local_settings.py
73
+ db.sqlite3
74
+
75
+ # Flask stuff:
76
+ instance/
77
+ .webassets-cache
78
+
79
+ # Scrapy stuff:
80
+ .scrapy
81
+
82
+ # Sphinx documentation
83
+ docs/_build/
84
+
85
+ # PyBuilder
86
+ target/
87
+
88
+ # Jupyter Notebook
89
+ .ipynb_checkpoints
90
+
91
+ # IPython
92
+ profile_default/
93
+ ipython_config.py
94
+
95
+ # pyenv
96
+ .python-version
97
+
98
+ # celery beat schedule file
99
+ celerybeat-schedule
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # node
129
+ node_modules/
CHANGELOG.md ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## v3.4.6 (2025/11/29)
4
+ * Add disable image, css, fonts option with CDP. Thanks @Ananto30
5
+
6
+ ## v3.4.5 (2025/11/11)
7
+ * Revert to Python v3.13
8
+
9
+ ## v3.4.4 (2025/11/04)
10
+ * Bump dependencies, Chromium, and some other general fixes. Thanks @flowerey
11
+
12
+ ## v3.4.3 (2025/10/28)
13
+ * Update proxy extension
14
+
15
+ ## v3.4.2 (2025/10/09)
16
+ * Bump dependencies & CI actions. Thanks @flowerey
17
+ * Add optional wait time after resolving the challenge before returning. Thanks @kennedyoliveira
18
+ * Add proxy ENVs. Thanks @Robokishan
19
+ * Handle empty string and keys without value in postData. Thanks @eZ4RK0
20
+ * Add quote protection for password containing it. Thanks @warrenberberd
21
+ * Add returnScreenshot parameter to screenshot the final web page. Thanks @estebanthi
22
+ * Add log file support. Thanks @acg5159
23
+
24
+ ## v3.4.1 (2025/09/15)
25
+ * Fix regex pattern syntax in utils.py
26
+ * Change access denied title check to use startswith
27
+
28
+ ## v3.4.0 (2025/08/25)
29
+ * Modernize and upgrade application. Thanks @TheCrazyLex
30
+ * Remove disable software rasterizer option for ARM builds. Thanks @smrodman83
31
+
32
+ ## v3.3.25 (2025/06/14)
33
+ * Remove `use-gl` argument. Thanks @qwerty12
34
+ * u_c: remove apparent c&p typo. Thanks @ok3721
35
+ * Bump requirements
36
+
37
+ ## v3.3.24 (2025/06/04)
38
+ * Remove hidden character
39
+
40
+ ## v3.3.23 (2025/06/04)
41
+ * Update base image to bookworm. Thanks @rwjack
42
+
43
+ ## v3.3.22 (2025/06/03)
44
+ * Disable search engine choice screen
45
+ * Fix headless=false stalling. Thanks @MAKMED1337
46
+ * Change from click to keys. Thanks @sh4dowb
47
+ * Don't open devtools
48
+ * Bump Chromium to v137 for build
49
+ * Bump requirements
50
+
51
+ ## v3.3.21 (2024/06/26)
52
+ * Add challenge selector to catch reloading page on non-English systems
53
+ * Escape values for generated form used in request.post. Thanks @mynameisbogdan
54
+
55
+ ## v3.3.20 (2024/06/21)
56
+ * maxTimeout should always be int
57
+ * Check not running in Docker before logging version_main error
58
+ * Update Cloudflare challenge and checkbox selectors. Thanks @tenettow & @21hsmw
59
+
60
+ ## v3.3.19 (2024/05/23)
61
+ * Fix occasional headless issue on Linux when set to "false". Thanks @21hsmw
62
+
63
+ ## v3.3.18 (2024/05/20)
64
+
65
+ * Fix LANG ENV for Linux
66
+ * Fix Chrome v124+ not closing on Windows. Thanks @RileyXX
67
+
68
+ ## v3.3.17 (2024/04/09)
69
+
70
+ * Fix file descriptor leak in service on quit(). Thanks @zkulis
71
+
72
+ ## v3.3.16 (2024/02/28)
73
+
74
+ * Fix of the subprocess.STARTUPINFO() call. Thanks @ceconelo
75
+ * Add FreeBSD support. Thanks @Asthowen
76
+ * Use headless configuration properly. Thanks @hashworks
77
+
78
+ ## v3.3.15 (2024/02/20)
79
+
80
+ * Fix looping challenges
81
+
82
+ ## v3.3.14-hotfix2 (2024/02/17)
83
+
84
+ * Hotfix 2 - bad Chromium build, instances failed to terminate
85
+
86
+ ## v3.3.14-hotfix (2024/02/17)
87
+
88
+ * Hotfix for Linux build - some Chrome files no longer exist
89
+
90
+ ## v3.3.14 (2024/02/17)
91
+
92
+ * Update Chrome downloads. Thanks @opemvbs
93
+
94
+ ## v3.3.13 (2024/01/07)
95
+
96
+ * Fix too many open files error
97
+
98
+ ## v3.3.12 (2023/12/15)
99
+
100
+ * Fix looping challenges and invalid cookies
101
+
102
+ ## v3.3.11 (2023/12/11)
103
+
104
+ * Update UC 3.5.4 & Selenium 4.15.2. Thanks @txtsd
105
+
106
+ ## v3.3.10 (2023/11/14)
107
+
108
+ * Add LANG ENV - resolves issues with YGGtorrent
109
+
110
+ ## v3.3.9 (2023/11/13)
111
+
112
+ * Fix for Docker build, capture TypeError
113
+
114
+ ## v3.3.8 (2023/11/13)
115
+
116
+ * Fix headless=true for Chrome 117+. Thanks @NabiKAZ
117
+ * Support running Chrome 119 from source. Thanks @koleg and @Chris7X
118
+ * Fix "OSError: [WinError 6] The handle is invalid" on exit. Thanks @enesgorkemgenc
119
+
120
+ ## v3.3.7 (2023/11/05)
121
+
122
+ * Bump to rebuild. Thanks @JoachimDorchies
123
+
124
+ ## v3.3.6 (2023/09/15)
125
+
126
+ * Update checkbox selector, again
127
+
128
+ ## v3.3.5 (2023/09/13)
129
+
130
+ * Change checkbox selector, support languages other than English
131
+
132
+ ## v3.3.4 (2023/09/02)
133
+
134
+ * Update checkbox selector
135
+
136
+ ## v3.3.3 (2023/08/31)
137
+
138
+ * Update undetected_chromedriver to v3.5.3
139
+
140
+ ## v3.3.2 (2023/08/03)
141
+
142
+ * Fix URL domain in Prometheus exporter
143
+
144
+ ## v3.3.1 (2023/08/03)
145
+
146
+ * Fix for Cloudflare verify checkbox
147
+ * Fix HEADLESS=false in Windows binary
148
+ * Fix Prometheus exporter for management and health endpoints
149
+ * Remove misleading stack trace when the verify checkbox is not found
150
+ * Revert "Update base Docker image to Debian Bookworm" #849
151
+ * Revert "Install Chromium 115 from Debian testing" #849
152
+
153
+ ## v3.3.0 (2023/08/02)
154
+
155
+ * Fix for new Cloudflare detection. Thanks @cedric-bour for #845
156
+ * Add support for proxy authentication username/password. Thanks @jacobprice808 for #807
157
+ * Implement Prometheus metrics
158
+ * Fix Chromium Driver for Chrome / Chromium version > 114
159
+ * Use Chromium 115 in binary packages (Windows and Linux)
160
+ * Install Chromium 115 from Debian testing (Docker)
161
+ * Update base Docker image to Debian Bookworm
162
+ * Update Selenium 4.11.2
163
+ * Update pyinstaller 5.13.0
164
+ * Add more traces in build_package.py
165
+
166
+ ## v3.2.2 (2023/07/16)
167
+
168
+ * Workaround for updated 'verify you are human' check
169
+
170
+ ## v3.2.1 (2023/06/10)
171
+
172
+ * Kill dead Chrome processes in Windows
173
+ * Fix Chrome GL erros in ASUSTOR NAS
174
+
175
+ ## v3.2.0 (2023/05/23)
176
+
177
+ * Support "proxy" param in requests and sessions
178
+ * Support "cookies" param in requests
179
+ * Fix Chromium exec permissions in Linux package
180
+ * Update Python dependencies
181
+
182
+ ## v3.1.2 (2023/04/02)
183
+
184
+ * Fix headless mode in macOS
185
+ * Remove redundant artifact from Windows binary package
186
+ * Bump Selenium dependency
187
+
188
+ ## v3.1.1 (2023/03/25)
189
+
190
+ * Distribute binary executables in compressed package
191
+ * Add icon for binary executable
192
+ * Include information about supported architectures in the readme
193
+ * Check Python version on start
194
+
195
+ ## v3.1.0 (2023/03/20)
196
+
197
+ * Build binaries for Linux x64 and Windows x64
198
+ * Sessions with auto-creation on fetch request and TTL
199
+ * Fix error trace: Crash Reports/pending No such file or directory
200
+ * Fix Waitress server error with asyncore_use_poll=true
201
+ * Attempt to fix Docker ARM32 build
202
+ * Print platform information on start up
203
+ * Add Fairlane challenge selector
204
+ * Update DDOS-GUARD title
205
+ * Update dependencies
206
+
207
+ ## v3.0.4 (2023/03/07)
208
+
209
+ * Click on the Cloudflare's 'Verify you are human' button if necessary
210
+
211
+ ## v3.0.3 (2023/03/06)
212
+
213
+ * Update undetected_chromedriver version to 3.4.6
214
+
215
+ ## v3.0.2 (2023/01/08)
216
+
217
+ * Detect Cloudflare blocked access
218
+ * Check Chrome / Chromium web browser is installed correctly
219
+
220
+ ## v3.0.1 (2023/01/06)
221
+
222
+ * Kill Chromium processes properly to avoid defunct/zombie processes
223
+ * Update undetected-chromedriver
224
+ * Disable Zygote sandbox in Chromium browser
225
+ * Add more selectors to detect blocked access
226
+ * Include procps (ps), curl and vim packages in the Docker image
227
+
228
+ ## v3.0.0 (2023/01/04)
229
+
230
+ * This is the first release of FlareSolverr v3. There are some breaking changes
231
+ * Docker images for linux/386, linux/amd64, linux/arm/v7 and linux/arm64/v8
232
+ * Replaced Firefox with Chrome
233
+ * Replaced NodeJS / Typescript with Python
234
+ * Replaced Puppeter with Selenium
235
+ * No binaries for Linux / Windows. You have to use the Docker image or install from Source code
236
+ * No proxy support
237
+ * No session support
238
+
239
+ ## v2.2.10 (2022/10/22)
240
+
241
+ * Detect DDoS-Guard through title content
242
+
243
+ ## v2.2.9 (2022/09/25)
244
+
245
+ * Detect Cloudflare Access Denied
246
+ * Commit the complete changelog
247
+
248
+ ## v2.2.8 (2022/09/17)
249
+
250
+ * Remove 30 s delay and clean legacy code
251
+
252
+ ## v2.2.7 (2022/09/12)
253
+
254
+ * Temporary fix: add 30s delay
255
+ * Update README.md
256
+
257
+ ## v2.2.6 (2022/07/31)
258
+
259
+ * Fix Cloudflare detection in POST requests
260
+
261
+ ## v2.2.5 (2022/07/30)
262
+
263
+ * Update GitHub actions to build executables with NodeJs 16
264
+ * Update Cloudflare selectors and add HTML samples
265
+ * Install Firefox 94 instead of the latest Nightly
266
+ * Update dependencies
267
+ * Upgrade Puppeteer (#396)
268
+
269
+ ## v2.2.4 (2022/04/17)
270
+
271
+ * Detect DDoS-Guard challenge
272
+
273
+ ## v2.2.3 (2022/04/16)
274
+
275
+ * Fix 2000 ms navigation timeout
276
+ * Update README.md (libseccomp2 package in Debian)
277
+ * Update README.md (clarify proxy parameter) (#307)
278
+ * Update NPM dependencies
279
+ * Disable Cloudflare ban detection
280
+
281
+ ## v2.2.2 (2022/03/19)
282
+
283
+ * Fix ban detection. Resolves #330 (#336)
284
+
285
+ ## v2.2.1 (2022/02/06)
286
+
287
+ * Fix max timeout error in some pages
288
+ * Avoid crashing in NodeJS 17 due to Unhandled promise rejection
289
+ * Improve proxy validation and debug traces
290
+ * Remove @types/puppeteer dependency
291
+
292
+ ## v2.2.0 (2022/01/31)
293
+
294
+ * Increase default BROWSER_TIMEOUT=40000 (40 seconds)
295
+ * Fix Puppeter deprecation warnings
296
+ * Update base Docker image Alpine 3.15 / NodeJS 16
297
+ * Build precompiled binaries with NodeJS 16
298
+ * Update Puppeter and other dependencies
299
+ * Add support for Custom CloudFlare challenge
300
+ * Add support for DDoS-GUARD challenge
301
+
302
+ ## v2.1.0 (2021/12/12)
303
+
304
+ * Add aarch64 to user agents to be replaced (#248)
305
+ * Fix SOCKSv4 and SOCKSv5 proxy. resolves #214 #220
306
+ * Remove redundant JSON key (postData) (#242)
307
+ * Make test URL configurable with TEST_URL env var. resolves #240
308
+ * Bypass new Cloudflare protection
309
+ * Update donation links
310
+
311
+ ## v2.0.2 (2021/10/31)
312
+
313
+ * Fix SOCKS5 proxy. Resolves #214
314
+ * Replace Firefox ERS with a newer version
315
+ * Catch startup exceptions and give some advices
316
+ * Add env var BROWSER_TIMEOUT for slow systems
317
+ * Fix NPM warning in Docker images
318
+
319
+ ## v2.0.1 (2021/10/24)
320
+
321
+ * Check user home dir before testing web browser installation
322
+
323
+ ## v2.0.0 (2021/10/20)
324
+
325
+ FlareSolverr 2.0.0 is out with some important changes:
326
+
327
+ * It is capable of solving the automatic challenges of Cloudflare. CAPTCHAs (hCaptcha) cannot be resolved and the old solvers have been removed.
328
+ * The Chrome browser has been replaced by Firefox. This has caused some functionality to be removed. Parameters: `userAgent`, `headers`, `rawHtml` and `downloadare` no longer available.
329
+ * Included `proxy` support without user/password credentials. If you are writing your own integration with FlareSolverr, make sure your client uses the same User-Agent header and Proxy that FlareSolverr uses. Those values together with the Cookie are checked and detected by Cloudflare.
330
+ * FlareSolverr has been rewritten from scratch. From now on it should be easier to maintain and test.
331
+ * If you are using Jackett make sure you have version v0.18.1041 or higher. FlareSolverSharp v2.0.0 is out too.
332
+
333
+ Complete changelog:
334
+
335
+ * Bump version 2.0.0
336
+ * Set puppeteer timeout half of maxTimeout param. Resolves #180
337
+ * Add test for blocked IP
338
+ * Avoid reloading the page in case of error
339
+ * Improve Cloudflare detection
340
+ * Fix version
341
+ * Fix browser preferences and proxy
342
+ * Fix request.post method and clean error traces
343
+ * Use Firefox ESR for Docker images
344
+ * Improve Firefox start time and code clean up
345
+ * Improve bad request management and tests
346
+ * Build native packages with Firefox
347
+ * Update readme
348
+ * Improve Docker image and clean TODOs
349
+ * Add proxy support
350
+ * Implement request.post method for Firefox
351
+ * Code clean up, remove returnRawHtml, download, headers params
352
+ * Remove outdated chaptcha solvers
353
+ * Refactor the app to use Express server and Jest for tests
354
+ * Fix Cloudflare resolver for Linux ARM builds
355
+ * Fix Cloudflare resolver
356
+ * Replace Chrome web browser with Firefox
357
+ * Remove userAgent parameter since any modification is detected by CF
358
+ * Update dependencies
359
+ * Remove Puppeter steath plugin
360
+
361
+ ## v1.2.9 (2021/08/01)
362
+
363
+ * Improve "Execution context was destroyed" error handling
364
+ * Implement returnRawHtml parameter. resolves #172 resolves #165
365
+ * Capture Docker stop signal. resolves #158
366
+ * Reduce Docker image size 20 MB
367
+ * Fix page reload after challenge is solved. resolves #162 resolves #143
368
+ * Avoid loading images/css/fonts to speed up page load
369
+ * Improve Cloudflare IP ban detection
370
+ * Fix vulnerabilities
371
+
372
+ ## v1.2.8 (2021/06/01)
373
+
374
+ * Improve old JS challenge waiting. Resolves #129
375
+
376
+ ## v1.2.7 (2021/06/01)
377
+
378
+ * Improvements in Cloudflare redirect detection. Resolves #140
379
+ * Fix installation instructions
380
+
381
+ ## v1.2.6 (2021/05/30)
382
+
383
+ * Handle new Cloudflare challenge. Resolves #135 Resolves #134
384
+ * Provide reference Systemd unit file. Resolves #72
385
+ * Fix EACCES: permission denied, open '/tmp/flaresolverr.txt'. Resolves #120
386
+ * Configure timezone with TZ env var. Resolves #109
387
+ * Return the redirected URL in the response (#126)
388
+ * Show an error in hcaptcha-solver. Resolves #132
389
+ * Regenerate package-lock.json lockfileVersion 2
390
+ * Update issue template. Resolves #130
391
+ * Bump ws from 7.4.1 to 7.4.6 (#137)
392
+ * Bump hosted-git-info from 2.8.8 to 2.8.9 (#124)
393
+ * Bump lodash from 4.17.20 to 4.17.21 (#125)
394
+
395
+ ## v1.2.5 (2021/04/05)
396
+
397
+ * Fix memory regression, close test browser
398
+ * Fix release-docker GitHub action
399
+
400
+ ## v1.2.4 (2021/04/04)
401
+
402
+ * Include license in release zips. resolves #75
403
+ * Validate Chrome is working at startup
404
+ * Speedup Docker image build
405
+ * Add health check endpoint
406
+ * Update issue template
407
+ * Minor improvements in debug traces
408
+ * Validate environment variables at startup. resolves #101
409
+ * Add FlareSolverr logo. resolves #23
410
+
411
+ ## v1.2.3 (2021/01/10)
412
+
413
+ * CI/CD: Generate release changelog from commits. resolves #34
414
+ * Update README.md
415
+ * Add donation links
416
+ * Simplify docker-compose.yml
417
+ * Allow to configure "none" captcha resolver
418
+ * Override docker-compose.yml variables via .env resolves #64 (#66)
419
+
420
+ ## v1.2.2 (2021/01/09)
421
+
422
+ * Add documentation for precompiled binaries installation
423
+ * Add instructions to set environment variables in Windows
424
+ * Build Windows and Linux binaries. resolves #18
425
+ * Add release badge in the readme
426
+ * CI/CD: Generate release changelog from commits. resolves #34
427
+ * Add a notice about captcha solvers
428
+ * Add Chrome flag --disable-dev-shm-usage to fix crashes. resolves #45
429
+ * Fix Docker CLI documentation
430
+ * Add traces with captcha solver service. resolves #39
431
+ * Improve logic to detect Cloudflare captcha. resolves #48
432
+ * Move Cloudflare provider logic to his own class
433
+ * Simplify and document the "return only cookies" parameter
434
+ * Show message when debug log is enabled
435
+ * Update readme to add more clarifications. resolves #53 (#60)
436
+ * issue_template: typo fix (#52)
437
+
438
+ ## v1.2.1 (2020/12/20)
439
+
440
+ * Change version to match release tag / 1.2.0 => v1.2.0
441
+ * CI/CD Publish release in GitHub repository. resolves #34
442
+ * Add welcome message in / endpoint
443
+ * Rewrite request timeout handling (maxTimeout) resolves #42
444
+ * Add http status for better logging
445
+ * Return an error when no selectors are found, #25
446
+ * Add issue template, fix #32
447
+ * Moving log.html right after loading the page and add one on reload, fix #30
448
+ * Update User-Agent to match chromium version, ref: #15 (#28)
449
+ * Update install from source code documentation
450
+ * Update readme to add Docker instructions (#20)
451
+ * Clean up readme (#19)
452
+ * Add docker-compose
453
+ * Change default log level to info
454
+
455
+ ## v1.2.0 (2020/12/20)
456
+
457
+ * Fix User-Agent detected by CouldFlare (Docker ARM) resolves #15
458
+ * Include exception message in error response
459
+ * CI/CD: Rename GitHub Action build => publish
460
+ * Bump version
461
+ * Fix TypeScript compilation and bump minor version
462
+ * CI/CD: Bump minor version
463
+ * CI/CD: Configure GitHub Actions
464
+ * CI/CD: Configure GitHub Actions
465
+ * CI/CD: Bump minor version
466
+ * CI/CD: Configure Build GitHub Action
467
+ * CI/CD: Configure AutoTag GitHub Action (#14)
468
+ * CI/CD: Build the Docker images with GitHub Actions (#13)
469
+ * Update dependencies
470
+ * Backport changes from Cloudproxy (#11)
Dockerfile ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Stage 1: Builder ---
2
+ FROM python:3.13-slim-bookworm AS builder
3
+
4
+ RUN apt-get update \
5
+ && apt-get install -y --no-install-recommends equivs \
6
+ && equivs-control libgl1-mesa-dri \
7
+ && printf 'Section: misc\nPriority: optional\nStandards-Version: 3.9.2\nPackage: libgl1-mesa-dri\nVersion: 99.0.0\nDescription: Dummy package\n' >> libgl1-mesa-dri \
8
+ && equivs-build libgl1-mesa-dri \
9
+ && mv libgl1-mesa-dri_*.deb /libgl1-mesa-dri.deb \
10
+ && equivs-control adwaita-icon-theme \
11
+ && printf 'Section: misc\nPriority: optional\nStandards-Version: 3.9.2\nPackage: adwaita-icon-theme\nVersion: 99.0.0\nDescription: Dummy package\n' >> adwaita-icon-theme \
12
+ && equivs-build adwaita-icon-theme \
13
+ && mv adwaita-icon-theme_*.deb /adwaita-icon-theme.deb [cite: 1, 2]
14
+
15
+ # --- Stage 2: Final Image ---
16
+ FROM python:3.13-slim-bookworm
17
+
18
+ # Hugging Face Spaces uses port 7860 by default
19
+ ENV PORT=7860
20
+ ENV HOME=/app
21
+
22
+ WORKDIR /app
23
+
24
+ # Copy dummy packages from builder
25
+ COPY --from=builder /*.deb /
26
+
27
+ # Install dependencies
28
+ RUN dpkg -i /libgl1-mesa-dri.deb \
29
+ && dpkg -i /adwaita-icon-theme.deb \
30
+ && apt-get update \
31
+ && apt-get install -y --no-install-recommends \
32
+ chromium chromium-common chromium-driver xvfb dumb-init \
33
+ procps curl vim xauth [cite: 3] \
34
+ && rm -rf /var/lib/apt/lists/* \
35
+ && rm -f /usr/lib/x86_64-linux-gnu/libmfxhw* \
36
+ && rm -f /usr/lib/x86_64-linux-gnu/mfx/* [cite: 3]
37
+
38
+ # Setup permissions for Hugging Face (which uses a random UID)
39
+ # We create a user but then make the directories group-writable
40
+ RUN useradd -m -u 1000 flaresolverr \
41
+ && mkdir -p /app/.config/chromium/Crash\ Reports/pending \
42
+ && mkdir -p /config \
43
+ && chown -R 1000:0 /app /config \
44
+ && chmod -R 775 /app /config [cite: 3, 4]
45
+
46
+ # Install Python dependencies
47
+ COPY requirements.txt .
48
+ RUN pip install --no-cache-dir -r requirements.txt [cite: 5]
49
+
50
+ # Copy source code
51
+ COPY src .
52
+ COPY package.json /app/package.json
53
+
54
+ # Ensure the entrypoint script or python command uses the HF port
55
+ EXPOSE 7860
56
+
57
+ # Use the flaresolverr user (UID 1000)
58
+ USER 1000
59
+
60
+ ENTRYPOINT ["/usr/bin/dumb-init", "--"]
61
+
62
+ # We pass the PORT environment variable to the python script
63
+ CMD ["sh", "-c", "/usr/local/bin/python -u /app/flaresolverr.py"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Diego Heras (ngosang / ngosang@hotmail.es)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,10 +1,347 @@
1
- ---
2
- title: Turnstile
3
- emoji: 🐨
4
- colorFrom: yellow
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FlareSolverr
2
+
3
+ [![Latest release](https://img.shields.io/github/v/release/FlareSolverr/FlareSolverr)](https://github.com/FlareSolverr/FlareSolverr/releases)
4
+ [![Docker Pulls](https://img.shields.io/docker/pulls/flaresolverr/flaresolverr)](https://hub.docker.com/r/flaresolverr/flaresolverr)
5
+ [![Docker Stars](https://img.shields.io/docker/stars/flaresolverr/flaresolverr)](https://hub.docker.com/r/flaresolverr/flaresolverr)
6
+ [![GitHub issues](https://img.shields.io/github/issues/FlareSolverr/FlareSolverr)](https://github.com/FlareSolverr/FlareSolverr/issues)
7
+ [![GitHub pull requests](https://img.shields.io/github/issues-pr/FlareSolverr/FlareSolverr)](https://github.com/FlareSolverr/FlareSolverr/pulls)
8
+ [![GitHub Repo stars](https://img.shields.io/github/stars/FlareSolverr/FlareSolverr)](https://github.com/FlareSolverr/FlareSolverr)
9
+
10
+ [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/ngosang)
11
+
12
+ FlareSolverr is a proxy server to bypass Cloudflare and DDoS-GUARD protection.
13
+
14
+ ## How it works
15
+
16
+ FlareSolverr starts a proxy server, and it waits for user requests in an idle state using few resources.
17
+ When some request arrives, it uses [Selenium](https://www.selenium.dev) with the
18
+ [undetected-chromedriver](https://github.com/ultrafunkamsterdam/undetected-chromedriver)
19
+ to create a web browser (Chrome). It opens the URL with user parameters and waits until the Cloudflare challenge
20
+ is solved (or timeout). The HTML code and the cookies are sent back to the user, and those cookies can be used to
21
+ bypass Cloudflare using other HTTP clients.
22
+
23
+ **NOTE**: Web browsers consume a lot of memory. If you are running FlareSolverr on a machine with few RAM, do not make
24
+ many requests at once. With each request a new browser is launched.
25
+
26
+ It is also possible to use a permanent session. However, if you use sessions, you should make sure to close them as
27
+ soon as you are done using them.
28
+
29
+ ## Installation
30
+
31
+ ### Docker
32
+
33
+ It is recommended to install using a Docker container because the project depends on an external browser that is
34
+ already included within the image.
35
+
36
+ Docker images are available in:
37
+
38
+ - GitHub Registry => https://github.com/orgs/FlareSolverr/packages/container/package/flaresolverr
39
+ - DockerHub => https://hub.docker.com/r/flaresolverr/flaresolverr
40
+
41
+ Supported architectures are:
42
+
43
+ | Architecture | Tag |
44
+ | ------------ | ------------ |
45
+ | x86 | linux/386 |
46
+ | x86-64 | linux/amd64 |
47
+ | ARM32 | linux/arm/v7 |
48
+ | ARM64 | linux/arm64 |
49
+
50
+ We provide a `docker-compose.yml` configuration file. Clone this repository and execute
51
+ `docker-compose up -d` _(Compose V1)_ or `docker compose up -d` _(Compose V2)_ to start
52
+ the container.
53
+
54
+ If you prefer the `docker cli` execute the following command:
55
+
56
+ **Bash**
57
+
58
+ ```bash
59
+ docker run -d \
60
+ --name=flaresolverr \
61
+ -p 8191:8191 \
62
+ -e LOG_LEVEL=info \
63
+ --restart unless-stopped \
64
+ ghcr.io/flaresolverr/flaresolverr:latest
65
+ ```
66
+
67
+ **Command Prompt or Powershell**
68
+
69
+ ```cmd
70
+ docker run -d --name=flaresolverr -p 8191:8191 -e LOG_LEVEL=info --restart unless-stopped ghcr.io/flaresolverr/flaresolverr:latest
71
+ ```
72
+
73
+ 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`
74
+ 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`.
75
+ Remember to restart the Docker daemon and the container after the update.
76
+
77
+ ### Precompiled binaries
78
+
79
+ > **Warning**
80
+ > Precompiled binaries are only available for x64 architecture. For other architectures see Docker images.
81
+
82
+ This is the recommended way for Windows users.
83
+
84
+ - Download the [FlareSolverr executable](https://github.com/FlareSolverr/FlareSolverr/releases) from the release's page. It is available for Windows x64 and Linux x64.
85
+ - Execute FlareSolverr binary. In the environment variables section you can find how to change the configuration.
86
+
87
+ ### From source code
88
+
89
+ > **Warning**
90
+ > Installing from source code only works for x64 architecture. For other architectures see Docker images.
91
+
92
+ - Install [Python 3.13](https://www.python.org/downloads/).
93
+ - 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.
94
+ - (Only in Linux) Install [Xvfb](https://en.wikipedia.org/wiki/Xvfb) package.
95
+ - (Only in macOS) Install [XQuartz](https://www.xquartz.org/) package.
96
+ - Clone this repository and open a shell in that path.
97
+ - Run `pip install -r requirements.txt` command to install FlareSolverr dependencies.
98
+ - Run `python src/flaresolverr.py` command to start FlareSolverr.
99
+
100
+ ### From source code (FreeBSD/TrueNAS CORE)
101
+
102
+ - Run `pkg install chromium python313 py313-pip xorg-vfbserver` command to install the required dependencies.
103
+ - Clone this repository and open a shell in that path.
104
+ - Run `python3.13 -m pip install -r requirements.txt` command to install FlareSolverr dependencies.
105
+ - Run `python3.13 src/flaresolverr.py` command to start FlareSolverr.
106
+
107
+ ### Systemd service
108
+
109
+ 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.
110
+
111
+ ## Usage
112
+
113
+ Example Bash request:
114
+
115
+ ```bash
116
+ curl -L -X POST 'http://localhost:8191/v1' \
117
+ -H 'Content-Type: application/json' \
118
+ --data-raw '{
119
+ "cmd": "request.get",
120
+ "url": "http://www.google.com/",
121
+ "maxTimeout": 60000
122
+ }'
123
+ ```
124
+
125
+ Example Python request:
126
+
127
+ ```py
128
+ import requests
129
+
130
+ url = "http://localhost:8191/v1"
131
+ headers = {"Content-Type": "application/json"}
132
+ data = {
133
+ "cmd": "request.get",
134
+ "url": "http://www.google.com/",
135
+ "maxTimeout": 60000
136
+ }
137
+ response = requests.post(url, headers=headers, json=data)
138
+ print(response.text)
139
+ ```
140
+
141
+ Example PowerShell request:
142
+
143
+ ```ps1
144
+ $body = @{
145
+ cmd = "request.get"
146
+ url = "http://www.google.com/"
147
+ maxTimeout = 60000
148
+ } | ConvertTo-Json
149
+
150
+ irm -UseBasicParsing 'http://localhost:8191/v1' -Headers @{"Content-Type"="application/json"} -Method Post -Body $body
151
+ ```
152
+
153
+ ### Commands
154
+
155
+ #### + `sessions.create`
156
+
157
+ This will launch a new browser instance which will retain cookies until you destroy it with `sessions.destroy`.
158
+ This comes in handy, so you don't have to keep solving challenges over and over and you won't need to keep sending
159
+ cookies for the browser to use.
160
+
161
+ This also speeds up the requests since it won't have to launch a new browser instance for every request.
162
+
163
+ | Parameter | Notes |
164
+ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
165
+ | session | Optional. The session ID that you want to be assigned to the instance. If isn't set a random UUID will be assigned. |
166
+ | 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"}` |
167
+
168
+ #### + `sessions.list`
169
+
170
+ Returns a list of all the active sessions. More for debugging if you are curious to see how many sessions are running.
171
+ You should always make sure to properly close each session when you are done using them as too many may slow your
172
+ computer down.
173
+
174
+ Example response:
175
+
176
+ ```json
177
+ {
178
+ "sessions": ["session_id_1", "session_id_2", "session_id_3..."]
179
+ }
180
+ ```
181
+
182
+ #### + `sessions.destroy`
183
+
184
+ This will properly shutdown a browser instance and remove all files associated with it to free up resources for a new
185
+ session. When you no longer need to use a session you should make sure to close it.
186
+
187
+ | Parameter | Notes |
188
+ | --------- | --------------------------------------------- |
189
+ | session | The session ID that you want to be destroyed. |
190
+
191
+ #### + `request.get`
192
+
193
+ | Parameter | Notes |
194
+ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
195
+ | url | Mandatory |
196
+ | 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. |
197
+ | session_ttl_minutes | Optional. FlareSolverr will automatically rotate expired sessions based on the TTL provided in minutes. |
198
+ | maxTimeout | Optional, default value 60000. Max timeout to solve the challenge in milliseconds. |
199
+ | cookies | Optional. Will be used by the headless browser. Eg: `"cookies": [{"name": "cookie1", "value": "value1"}, {"name": "cookie2", "value": "value2"}]`. |
200
+ | returnOnlyCookies | Optional, default false. Only returns the cookies. Response data, headers and other parts of the response are removed. |
201
+ | 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. |
202
+ | 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`.) |
203
+ | 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. |
204
+ | disableMedia | Optional, default false. When true FlareSolverr will prevent media resources (images, CSS, and fonts) from being loaded to speed up navigation. |
205
+ | 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`. |
206
+
207
+ > **Warning**
208
+ > 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.
209
+
210
+ Example response from running the `curl` above:
211
+
212
+ ```json
213
+ {
214
+ "solution": {
215
+ "url": "https://www.google.com/?gws_rd=ssl",
216
+ "status": 200,
217
+ "headers": {
218
+ "status": "200",
219
+ "date": "Thu, 16 Jul 2020 04:15:49 GMT",
220
+ "expires": "-1",
221
+ "cache-control": "private, max-age=0",
222
+ "content-type": "text/html; charset=UTF-8",
223
+ "strict-transport-security": "max-age=31536000",
224
+ "p3p": "CP=\"This is not a P3P policy! See g.co/p3phelp for more info.\"",
225
+ "content-encoding": "br",
226
+ "server": "gws",
227
+ "content-length": "61587",
228
+ "x-xss-protection": "0",
229
+ "x-frame-options": "SAMEORIGIN",
230
+ "set-cookie": "1P_JAR=2020-07-16-04; expires=Sat..."
231
+ },
232
+ "response": "<!DOCTYPE html>...",
233
+ "cookies": [
234
+ {
235
+ "name": "NID",
236
+ "value": "204=QE3Ocq15XalczqjuDy52HeseG3zAZuJzID3R57...",
237
+ "domain": ".google.com",
238
+ "path": "/",
239
+ "expires": 1610684149.307722,
240
+ "size": 178,
241
+ "httpOnly": true,
242
+ "secure": true,
243
+ "session": false,
244
+ "sameSite": "None"
245
+ },
246
+ {
247
+ "name": "1P_JAR",
248
+ "value": "2020-07-16-04",
249
+ "domain": ".google.com",
250
+ "path": "/",
251
+ "expires": 1597464949.307626,
252
+ "size": 19,
253
+ "httpOnly": false,
254
+ "secure": true,
255
+ "session": false,
256
+ "sameSite": "None"
257
+ }
258
+ ],
259
+ "userAgent": "Windows NT 10.0; Win64; x64) AppleWebKit/5...",
260
+ "turnstile_token": "03AGdBq24k3lK7JH2v8uN1T5F..."
261
+ },
262
+ "status": "ok",
263
+ "message": "",
264
+ "startTimestamp": 1594872947467,
265
+ "endTimestamp": 1594872949617,
266
+ "version": "1.0.0"
267
+ }
268
+ ```
269
+
270
+ ### + `request.post`
271
+
272
+ 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.
273
+
274
+ | Parameter | Notes |
275
+ | --------- | ------------------------------------------------------------------------ |
276
+ | postData | Must be a string with `application/x-www-form-urlencoded`. Eg: `a=b&c=d` |
277
+
278
+ ## Environment variables
279
+
280
+ | Name | Default | Notes |
281
+ | ------------------ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
282
+ | LOG_LEVEL | info | Verbosity of the logging. Use `LOG_LEVEL=debug` for more information. |
283
+ | LOG_FILE | none | Path to capture log to file. Example: `/config/flaresolverr.log`. |
284
+ | LOG_HTML | false | Only for debugging. If `true` all HTML that passes through the proxy will be logged to the console in `debug` level. |
285
+ | PROXY_URL | none | URL for proxy. Will be overwritten by `request` or `sessions` proxy, if used. Example: `http://127.0.0.1:8080`. |
286
+ | PROXY_USERNAME | none | Username for proxy. Will be overwritten by `request` or `sessions` proxy, if used. Example: `testuser`. |
287
+ | PROXY_PASSWORD | none | Password for proxy. Will be overwritten by `request` or `sessions` proxy, if used. Example: `testpass`. |
288
+ | CAPTCHA_SOLVER | none | Captcha solving method. It is used when a captcha is encountered. See the Captcha Solvers section. |
289
+ | TZ | UTC | Timezone used in the logs and the web browser. Example: `TZ=Europe/London`. |
290
+ | LANG | none | Language used in the web browser. Example: `LANG=en_GB`. |
291
+ | HEADLESS | true | Only for debugging. To run the web browser in headless mode or visible. |
292
+ | DISABLE_MEDIA | false | To disable loading images, CSS, and other media in the web browser to save network bandwidth. |
293
+ | 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. |
294
+ | PORT | 8191 | Listening port. You don't need to change this if you are running on Docker. |
295
+ | HOST | 0.0.0.0 | Listening interface. You don't need to change this if you are running on Docker. |
296
+ | PROMETHEUS_ENABLED | false | Enable Prometheus exporter. See the Prometheus section below. |
297
+ | PROMETHEUS_PORT | 8192 | Listening port for Prometheus exporter. See the Prometheus section below. |
298
+
299
+ Environment variables are set differently depending on the operating system. Some examples:
300
+
301
+ - 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.
302
+ - Linux: Run `export LOG_LEVEL=debug` and then run `flaresolverr` in the same shell.
303
+ - Windows: Open `cmd.exe`, run `set LOG_LEVEL=debug` and then run `flaresolverr.exe` in the same shell.
304
+
305
+ ## Prometheus exporter
306
+
307
+ 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`.
308
+
309
+ Example metrics:
310
+
311
+ ```shell
312
+ # HELP flaresolverr_request_total Total requests with result
313
+ # TYPE flaresolverr_request_total counter
314
+ flaresolverr_request_total{domain="nowsecure.nl",result="solved"} 1.0
315
+ # HELP flaresolverr_request_created Total requests with result
316
+ # TYPE flaresolverr_request_created gauge
317
+ flaresolverr_request_created{domain="nowsecure.nl",result="solved"} 1.690141657157109e+09
318
+ # HELP flaresolverr_request_duration Request duration in seconds
319
+ # TYPE flaresolverr_request_duration histogram
320
+ flaresolverr_request_duration_bucket{domain="nowsecure.nl",le="0.0"} 0.0
321
+ flaresolverr_request_duration_bucket{domain="nowsecure.nl",le="10.0"} 1.0
322
+ flaresolverr_request_duration_bucket{domain="nowsecure.nl",le="25.0"} 1.0
323
+ flaresolverr_request_duration_bucket{domain="nowsecure.nl",le="50.0"} 1.0
324
+ flaresolverr_request_duration_bucket{domain="nowsecure.nl",le="+Inf"} 1.0
325
+ flaresolverr_request_duration_count{domain="nowsecure.nl"} 1.0
326
+ flaresolverr_request_duration_sum{domain="nowsecure.nl"} 5.858
327
+ # HELP flaresolverr_request_duration_created Request duration in seconds
328
+ # TYPE flaresolverr_request_duration_created gauge
329
+ flaresolverr_request_duration_created{domain="nowsecure.nl"} 1.6901416571570296e+09
330
+ ```
331
+
332
+ ## Captcha Solvers
333
+
334
+ > **Warning**
335
+ > At this time none of the captcha solvers work. You can check the status in the open issues. Any help is welcome.
336
+
337
+ Sometimes CloudFlare not only gives mathematical computations and browser tests, sometimes they also require the user to
338
+ solve a captcha.
339
+ If this is the case, FlareSolverr will return the error `Captcha detected but no automatic solver is configured.`
340
+
341
+ FlareSolverr can be customized to solve the CAPTCHA automatically by setting the environment variable `CAPTCHA_SOLVER`
342
+ to the file name of one of the adapters inside the `/captcha` directory.
343
+
344
+ ## Related projects
345
+
346
+ - C# implementation => https://github.com/FlareSolverr/FlareSolverrSharp
347
+
docker-compose.yml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ version: "2.1"
3
+ services:
4
+ flaresolverr:
5
+ # DockerHub mirror flaresolverr/flaresolverr:latest
6
+ image: ghcr.io/flaresolverr/flaresolverr:latest
7
+ container_name: flaresolverr
8
+ environment:
9
+ - LOG_LEVEL=${LOG_LEVEL:-info}
10
+ - LOG_FILE=${LOG_FILE:-none}
11
+ - LOG_HTML=${LOG_HTML:-false}
12
+ - CAPTCHA_SOLVER=${CAPTCHA_SOLVER:-none}
13
+ - TZ=Europe/London
14
+ ports:
15
+ - "${PORT:-8191}:8191"
16
+ volumes:
17
+ - /var/lib/flaresolver:/config
18
+ restart: unless-stopped
flaresolverr.service ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [Unit]
2
+ Description=FlareSolverr
3
+ After=network.target
4
+
5
+ [Service]
6
+ SyslogIdentifier=flaresolverr
7
+ Restart=always
8
+ RestartSec=5
9
+ Type=simple
10
+ User=flaresolverr
11
+ Group=flaresolverr
12
+ Environment="LOG_LEVEL=info"
13
+ Environment="CAPTCHA_SOLVER=none"
14
+ WorkingDirectory=/opt/flaresolverr
15
+ ExecStart=/opt/flaresolverr/flaresolverr
16
+ TimeoutStopSec=30
17
+
18
+ [Install]
19
+ WantedBy=multi-user.target
html_samples/cloudflare_captcha_hcaptcha_v1.html ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+
4
+ <head>
5
+ <title>Just a moment...</title>
6
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
7
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge">
8
+ <meta name="robots" content="noindex,nofollow">
9
+ <meta name="viewport" content="width=device-width,initial-scale=1">
10
+ <link href="Just%20a%20moment_files/cf-errors.css" rel="stylesheet">
11
+
12
+ <script>
13
+ (function () {
14
+ window._cf_chl_opt = {
15
+ cvId: '2',
16
+ cType: 'managed',
17
+ cNounce: '67839',
18
+ cRay: '732fbc436ab471ed',
19
+ cHash: 'dce5bd920f3aa51',
20
+ cUPMDTk: "\/search?q=2022&__cf_chl_tk=lkycIb1jDXlmFqiB7AXTwy38_EzYPvu79CCQyU9lhUE-1659201316-0-gaNycGzNCf0",
21
+ cFPWv: 'g',
22
+ cTTimeMs: '1000',
23
+ cTplV: 2,
24
+ cRq: {
25
+ ru: 'aHR0cHM6Ly8wbWFnbmV0LmNvbS9zZWFyY2g/cT0yMDIy',
26
+ ra: 'TW96aWxsYS81LjAgKFgxMTsgTGludXggeDg2XzY0OyBydjoxMDUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC8xMDUuMA==',
27
+ rm: 'R0VU',
28
+ d: 'MqxNbbGfWazPaVMZ7GQRz02TV/pSUL9POWx0y4e7HFRwP1RTAxLc1RZRuHg+N/bGMuPj08kSx0UpcjEjMkSOqiU6I/64IDYbCJvey5rY07fkkljpZaYGTDZIoWdOWlgP3ky15ybZ42xMK4tfI1yJ+iFZCVgR6VBjJzi5I56j9Ijog2AvsoQW2TrguGpgKaT1LkhxWNElzBbvXWt1uyRgE19UQ9J/5vtxEwoh5wodHh7WE297n8uI1hpDgge2bDYQvwe+RDq3QAyhQOmymg+IIlt1y115v9R8k5ehT9TFY3vYvYnoJu9cOyHYprf9Z0jTNGxSTvLHYJbfq30Samu5fKfE0oZREZizvPUgUsJm2rRKkCY9VCdBkpO8vaUgIwIYkeWavtqdudjb3zEDBCD4cAH/xv3Bl1VRy2Qf7XlcbpElCOq06TDTQ1uGjyCqbVbvjesrOy0Dp2nXTjdfbkWvnN7mWpFlPUD7/41MUo9lc6V1Aj1Kjg6AKfVV4DUHpq6ZVnMHzrcPQLy4qD7CptcMpQKArZtJCRsUpgq8GWKJcU4dU8ZmyROAA+l+JEVnGbh2bsRdif4azh57OdjZfEKSa5c+AL3i66vyWAZCw9Wl6CAQdFTA+ixkbl8zKbCm8ulv',
29
+ t: 'MTY1OTIwMTMxNi4zOTIwMDA=',
30
+ m: '3l81qRkXiMTbjTzBtc0v1XwSheF46UfagbXVhYgbAVw=',
31
+ i1: 'Iu5a1gH3p9igzqBwncow9g==',
32
+ i2: 'PmNXozjc73unhnp/X0+kUQ==',
33
+ zh: 'qP4bnGc6j96JlnjNSE7HmQci3S9L50bHFtm4bQRjjKU=',
34
+ uh: 'SK3PXNkeRzZtkRARhJpbmZpCIiWQw6+5gpOE7vojWx4=',
35
+ hh: 'azXzJl8Ou22g0nN/9idVUoB9EqZ7fLmkSdDRHM3Lkmw=',
36
+ }
37
+ }
38
+ window._cf_chl_enter = function () { window._cf_chl_opt.p = 1 };
39
+ })();
40
+ </script>
41
+
42
+ <script src="Just%20a%20moment_files/v1.js"></script>
43
+ <script type="text/javascript" src="Just%20a%20moment_files/api.js"></script>
44
+ </head>
45
+
46
+ <body class="no-js">
47
+
48
+ <div class="privacy-pass">
49
+ <a rel="noopener noreferrer" href="https://addons.mozilla.org/en-US/firefox/addon/privacy-pass/"
50
+ target="_blank">
51
+ Privacy Pass
52
+ <span class="privacy-pass-icon-wrapper">
53
+ <div class="privacy-pass-icon"></div>
54
+ </span>
55
+ </a>
56
+ </div>
57
+
58
+ <div class="main-wrapper" role="main">
59
+ <div class="main-content">
60
+ <h1 class="zone-name-title h1">
61
+ <img class="heading-favicon" src="Just%20a%20moment_files/favicon.ico"
62
+ onerror="this.onerror=null;this.parentNode.removeChild(this)">
63
+ 0MAGNET.COM
64
+ </h1>
65
+ <h2 class="h2" id="cf-challenge-running">
66
+ Checking if the site connection is secure
67
+ </h2>
68
+ <div id="cf-challenge-stage" style="display: block;">
69
+ <div id="cf-challenge-hcaptcha-wrapper" class="captcha-prompt spacer">
70
+ <div style="display: none;" class="hcaptcha-box"><iframe src="Just%20a%20moment_files/hcaptcha.html"
71
+ title="widget containing checkbox for hCaptcha security challenge" tabindex="0"
72
+ scrolling="no" data-hcaptcha-widget-id="0tiueg8lyuj" data-hcaptcha-response=""
73
+ style="width: 303px; height: 78px; overflow: hidden;" frameborder="0"></iframe><textarea
74
+ id="h-captcha-response-0tiueg8lyuj" name="h-captcha-response"
75
+ style="display: none;"></textarea></div>
76
+ <div class="hcaptcha-box"><iframe src="Just%20a%20moment_files/hcaptcha_002.html"
77
+ title="widget containing checkbox for hCaptcha security challenge" tabindex="0"
78
+ scrolling="no" data-hcaptcha-widget-id="10tlmhzz0qyq" data-hcaptcha-response=""
79
+ style="width: 303px; height: 78px; overflow: hidden;" frameborder="0"></iframe><textarea
80
+ id="h-captcha-response-10tlmhzz0qyq" name="h-captcha-response"
81
+ style="display: none;"></textarea></div>
82
+ </div>
83
+ </div>
84
+ <div id="cf-challenge-spinner" class="spacer loading-spinner" style="display: none; visibility: hidden;">
85
+ <div class="lds-ring">
86
+ <div></div>
87
+ <div></div>
88
+ <div></div>
89
+ <div></div>
90
+ </div>
91
+ </div>
92
+ <noscript>
93
+ <div id="cf-challenge-error-title">
94
+ <div class="h2">
95
+ <span class="icon-wrapper">
96
+ <div class="heading-icon warning-icon"></div>
97
+ </span>
98
+ <span id="cf-challenge-error-text">
99
+ Enable JavaScript and cookies to continue
100
+ </span>
101
+ </div>
102
+ </div>
103
+ </noscript>
104
+ <div
105
+ style="display:none;background-image:url('/cdn-cgi/images/trace/captcha/nojs/transparent.gif?ray=732fbc436ab471ed')">
106
+ </div>
107
+ <div id="cf-challenge-body-text" class="core-msg spacer">
108
+ 0magnet.com needs to review the security of your connection before
109
+ proceeding.
110
+ </div>
111
+ <div id="cf-challenge-fact-wrapper" style="display: block; visibility: visible;" class="fact spacer hidden">
112
+ <span class="fact-title">Did you know</span> <span id="cf-challenge-fact" class="body-text">the first
113
+ botnet in 2003 took over 500-1000 devices? Today, botnets take over millions of devices at
114
+ once.</span>
115
+ </div>
116
+ <div id="cf-challenge-explainer-expandable" class="hidden expandable body-text spacer"
117
+ style="display: block; visibility: visible;">
118
+ <div class="expandable-title" id="cf-challenge-explainer-summary"><button class="expandable-summary-btn"
119
+ id="cf-challenge-explainer-btn" type="button"> Why am I seeing this page? <span
120
+ class="caret-icon-wrapper">
121
+ <div class="caret-icon"></div>
122
+ </span> </button> </div>
123
+ <div class="expandable-details" id="cf-challenge-explainer-details">
124
+ Requests from malicious bots can pose as legitimate traffic.
125
+ Occasionally, you may see this page while the site ensures that the
126
+ connection is secure.</div>
127
+ </div>
128
+ <div id="cf-challenge-success" style="display: none;">
129
+ <div class="h2"><span class="icon-wrapper"><img class="heading-icon" alt="Success icon"
130
+ src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAMAAADypuvZAAAANlBMVEUAAAAxMTEwMDAxMTExMTEwMDAwMDAwMDAxMTExMTExMTEwMDAwMDAxMTExMTEwMDAwMDAxMTHB9N+uAAAAEXRSTlMA3zDvfyBAEJC/n3BQz69gX7VMkcMAAAGySURBVEjHnZZbFoMgDEQJiDzVuv/NtgbtFGuQ4/zUKpeMIQbUhXSKE5l1XSn4pFWHRm/WShT1HRLWC01LGxFEVkCc30eYkLJ1Sjk9pvkw690VY6k8DWP9OM9yMG0Koi+mi8XA36NXmW0UXra4eJ3iwHfrfXVlgL0NqqGBHdqfeQhMmyJ48WDuKP81h3+SMPeRKkJcSXiLUK4XTHCjESOnz1VUXQoc6lgi2x4cI5aTQ201Mt8wHysI5fc05M5c81uZEtHcMKhxZ7iYEty1GfhLvGKpm+EYkdGxm1F5axmcB93DoORIbXfdN7f+hlFuyxtDP+sxtBnF43cIYwaZAWRgzxIoiXEMESoPlMhwLRDXeK772CAzXEdBRV7cmnoVBp0OSlyGidEzJTFq5hhcsA5388oSGM6b5p+qjpZrBlMS9xj4AwXmz108ukU1IomM3ceiW0CDwHCqp1NjAqXlFrbga+xuloQJ+tuyfbIBPNpqnmxqT7dPaOnZqBfhSBCteJAxWj58zLk2xgg+SPGYM6dRO6WczSnIxxwEExRaO+UyCUhbOp7CGQ+kxSUfNtLQFC+Po29vvy7jj4y0yAAAAABJRU5ErkJggg=="></span>Connection
131
+ is secure</div>
132
+ <div class="core-msg spacer">Proceeding...</div>
133
+ </div>
134
+ <form id="challenge-form"
135
+ action="/search?q=2022&amp;__cf_chl_f_tk=lkycIb1jDXlmFqiB7AXTwy38_EzYPvu79CCQyU9lhUE-1659201316-0-gaNycGzNCf0"
136
+ method="POST" enctype="application/x-www-form-urlencoded">
137
+ <input type="hidden" name="md"
138
+ value="P4fDbSohR3e3VZmGdBSN0Gd8t8ueht.ZVgSdQYwa45Y-1659201316-0-AesEKnKN8eJLiLESJle3R0T3fwKbVMlX09CR0sIU1LruDXen0nSlT2a5OpMUFYR7HQMGcF9Ja227n2p2D2ffUlWHPVeFX-YSNiewLZA3XuAQmOn-1DyWKA-SaMH_MW2vOSC7PCHAdJDhoRWjM_o3MyKziopj3WmDcaCI_ikk68bJTIValZ_e9tO7hmHC8zjsxDC8kXmI0tbrhyW5nyS2hRlx_ZVRcRHbHsVRN0-FGtEbCoaHmnp-q0N4AYhCJXofYRunPcSG_Y1iWMk-7ofOXON_gO7oGG_8-WWD5EG1jaz2ldpNO1RTkS7dQvTiC1Io1qAsVnQtokEaDR2zoWK_MF-hz6tOmuJIDgnAoH6vPFAa9EyJOUiG2RV-3q1CKTUgr82XRJw5CaXpN0QeBq0xHxFl5mzkFO8xqQsRnPkGUKtxBQ58syPIhR4AvNp8HA028gUNmaztJZ9i2UcWydut4VghHsoJjS5DEKTamjJhNrrkargjXUekXTfKXMVKCXxo0NFObTmKwzsNB5hrk3M43KzZCOOgTnqsrVUk54bAeDsr4qmTVW2wVk-0u78QpV2JFFOIJxRLikPmqo9CUokgUJ_IPsEjA5Q3kjrf9yq2OHU0MkwzLFNOAyc5N3A4WSYp91kESwxM98qFetpAZ0R3LID2c2-MraHnpOI2Xn4bxbDIdUPmjy6VB8Huuuf6M-o3Tw">
139
+ <input type="hidden" name="r"
140
+ value="bdZ7.nm8dGOZxq3EDOv_Kx7nKVv68q7b0RARXAlR9kQ-1659201316-0-AawyK3x4GgWasA2OtBBEp9Ea52qs8zEWwnQJxLWUnC+1jqlxaKHTIHeVQjvrTl/ccu6QA41yrSTKvazKiv6zQEiDj/6ziYkhldx+oJ7SqgMzPozzza1jofsGpCCPAzIlDicF+7sh4WKOxUJOeHgHCgfEZF/MPNsaahvbQ10U8Ei9tmvj8c2tkoybya75Bj5XHPPu0S9hnOH7S24ltm9vmyHlttI7uuI962FzPCTGjuAl4R/5+06WVAzBCJrS4biDNIuyYe22PtLl4b3Yf55eW7AFgyzKgddsohZJuNNliKyD6cusHDhm7MYpnXc5zwTdCbt6KGK/tBaylNyYwH/WBAUhyRYN5EVt9/iIKHrb+P6Z0RL4nO3BtQE/Zwx1VC3g1Wy4PPQJjqLixQptzl5eIzu43JIO/LBvT/mWuheH4eoPlghvyMYwfHcs7B4d7FCv1Tj9Skp9Fcj6HBAZlq/ss/eIwk7oOcTviQs+EUF9/yYatgtpXX9RCyvhMU6/ghOLfXRmOpAzsmoGnVqEpc2IMlZegYtieLveXU35cGJMI6wCR2ciCJIX995vLuL/4BdCAMEhyMAUWxtaCD2ZfRHyOWKNuf80w9k6/Ofhu7RevCr2mjQJAVTyE2OWWgOUuYJ4pZim93J7slMXieL3S5/JM08Q8g179Of7dzpN/oG7s80ljxAiCprpUAwpEmNiqNJN//v0e9KxknhCHeAWSAe8IeXbp5PSEQHXTmsqOFRkpud1pTsETcNbdonk8XMyv8mZRcFPVWRRWUb8hupn/d+x9r6mOdKdJkH8ZZ0R30LG0SLPYEvsVr2yU9o+uCZrRWkuE3SP3Lq3BIx+0vtm0DOvj6cODxy5/4Zm4x7LIpSa9wr69Rs2x+t+U5ydUupZ7oiAbWfYZSXHpmB0zJYOLMPJZcut50J/IgWuTMda8QBcTG3jRr4BTwpcmBZRmddfOJYgD7EMpOi1HgwLnS7l5QELafaMn0Hl6G774GVy4lEK2jURG9IEE3PV1m5Y903pqldFkJQsMxdisJWOzVjbtf41fxxnt4cQgiDQhktqCwg8xP6ijzPeWgvQHL5fMq61cQ5/4HB+yt9wKWMlBfUJR+ocI2MYx2nUWz+0BwCnTU29D9bx1xkir9bsnUnfOlfRDO2OEvI0iTe82666rVQO9XqTEz3POxrJYzLcSC9fTHpHfmCVwT2zWGGLi6pW5kqZh/uzSQ12MSvF5+dwvhe7yRks5gwMhnDHMQFyxKw3Xxm+dq2Ix/1uUucOhCu2L72j/NIwkF3Z7O7afY9nIu+NqMe8PbPJjq5ovEluosQfAMzWJH5Va8iur8o6K1y6hm7XFdNYAR+uCtxMw6WzF58QWVXXrDvfPeBMaNz+VVCnGP9elAwv62tc4Uh2SCbKbWdZchtLHgrJgYgQtCMhBDh0AXzE6ubbtfm9jE2vWcPj5jbo8U72i1pL2j8Xfr562Xc2WrQ7tKvSFQepGxfu2XgF7q55XKVqrnrBeXxZViUkB/gyXxI26CfrVfPLW+sYUo3JS+eCjyn2K7phv+630ixdpKrRJCTmkP3G8tcoLTJCB67/pbz+dXiNSB4JlHf4i3FVRkr8TAWS2zuMjJhB+ZyxnrGq/m7KSwpEEqgSCpOrQ5nkeoKIOyITfe9EPBSy9QtYDK+SAhUiLnICVURK7kGgrhZuKyK5/nyK9l7ffg16aaChJBisPBeiYsTDHlAeq0GbW7VR/jQDAVtVldeyD/dM5rJ4X+wl3A+faYD1OUxYT3n8dMs+E/1jLnYixXJpo7iXCqlTV3phOatg4XDQ5Bj6EYQIljVI4x2e8XHspcETIa0WepLsZF7WUtY7KbN8ZyDBFXgTMb4lzPmWyY8hZ05uX8EBKqUJhWh91AUob/OpJdf+u3axDDeRgsjl8K6CFM/5uQKo93co3KPqGZiqx0JoVj1t6KGxkrzYsgwlrTyeL44cEgr0zRQz5oFExuwHGYyogbHZ8EvU1eoiJ3IuQFxUH/1ULidfGtB371RYfz9gqONi1KiVzJ+zLjw+4HgMKXOV+ra09Dyg+eyUNfHillLXkhKWVoDpUhc+r46W6vXFp3oMKUWTRM0dE7iHofo+0tHb73d3ID2blRXgUeoMCQwOptoAYlFBIUYjggrIhd1AMC8TiZmiNULyP5imDePwcfq+ZjGH3o8VKRI2FcoFmChQegGco6pEbB5DxCguDuJbFRwGH4t9T0y74ZhlZiTNKA4xXsQnfIBEC5qz3mkcDAWoe73zqFAjp35JRVBjo3UDvehJppxzuoCXt9UbeuNEGll5/YJR4lfbUsEai0U6TFVleTTY53ofYCWEM6EnNDIToTFbm514YFTUSc4h8Qlq2fPeqC3IcCmirNT4Kf0FCO7MQrtGNFPJme2cpb/pZguS3pxxkKb4lOS+eGiUBGcSs1v3zHroJ+hum4wTJFRG0Yb99aCVQU44wgV3nKW7FZkXzwO3QY7nnkFI2kaAXerCPF4+Ho463g==">
141
+
142
+ <span style="display: none;"><span class="text-gray-600" data-translate="error">error code:
143
+ 1020</span></span>
144
+ </form>
145
+ </div>
146
+ </div>
147
+ <script>
148
+ (function () {
149
+ var trkjs = document.createElement('img');
150
+ trkjs.setAttribute('src', '/cdn-cgi/images/trace/captcha/js/transparent.gif?ray=732fbc436ab471ed');
151
+ trkjs.setAttribute('style', 'display: none');
152
+ document.body.appendChild(trkjs);
153
+ var cpo = document.createElement('script');
154
+ cpo.src = '/cdn-cgi/challenge-platform/h/g/orchestrate/managed/v1?ray=732fbc436ab471ed';
155
+ window._cf_chl_opt.cOgUHash = location.hash === '' && location.href.indexOf('#') !== -1 ? '#' : location.hash;
156
+ window._cf_chl_opt.cOgUQuery = location.search === '' && location.href.slice(0, -window._cf_chl_opt.cOgUHash.length).indexOf('?') !== -1 ? '?' : location.search;
157
+ if (window.history && window.history.replaceState) {
158
+ var ogU = location.pathname + window._cf_chl_opt.cOgUQuery + window._cf_chl_opt.cOgUHash;
159
+ history.replaceState(null, null, "\/search?q=2022&__cf_chl_rt_tk=lkycIb1jDXlmFqiB7AXTwy38_EzYPvu79CCQyU9lhUE-1659201316-0-gaNycGzNCf0" + window._cf_chl_opt.cOgUHash);
160
+ cpo.onload = function () {
161
+ history.replaceState(null, null, ogU);
162
+ };
163
+ }
164
+ document.getElementsByTagName('head')[0].appendChild(cpo);
165
+ }());
166
+ </script><img src="Just%20a%20moment_files/transparent.gif" style="display: none">
167
+
168
+ <div class="footer" role="contentinfo">
169
+ <div class="footer-inner">
170
+ <div class="clearfix diagnostic-wrapper">
171
+ <div class="ray-id">Ray ID: <code>732fbc436ab471ed</code></div>
172
+ </div>
173
+ <div class="text-center">
174
+ Performance &amp; security by
175
+ <a rel="noopener noreferrer" href="https://www.cloudflare.com/" target="_blank">Cloudflare</a>
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+
181
+ <div style="background-color: rgb(255, 255, 255); border: 1px solid rgb(215, 215, 215); box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 4px; border-radius: 4px; left: -10000px; top: -10000px; z-index: -2147483648; position: absolute; transition: opacity 0.15s ease-out 0s; opacity: 0; visibility: hidden;"
182
+ aria-hidden="true">
183
+ <div style="position: relative; z-index: 1;"><iframe src="Just%20a%20moment_files/hcaptcha_003.html"
184
+ title="Main content of the hCaptcha challenge" scrolling="no"
185
+ style="border: 0px none; z-index: 2000000000; position: relative;" frameborder="0"></iframe></div>
186
+ <div
187
+ style="width: 100%; height: 100%; position: fixed; pointer-events: none; top: 0px; left: 0px; z-index: 0; background-color: rgb(255, 255, 255); opacity: 0.05;">
188
+ </div>
189
+ <div
190
+ style="border-width: 11px; position: absolute; pointer-events: none; margin-top: -11px; z-index: 1; right: 100%;">
191
+ <div
192
+ style="border-width: 10px; border-style: solid; border-color: transparent rgb(255, 255, 255) transparent transparent; position: relative; top: 10px; z-index: 1;">
193
+ </div>
194
+ <div
195
+ style="border-width: 11px; border-style: solid; border-color: transparent rgb(215, 215, 215) transparent transparent; position: relative; top: -11px; z-index: 0;">
196
+ </div>
197
+ </div>
198
+ </div>
199
+ <div style="background-color: rgb(255, 255, 255); border: 1px solid rgb(215, 215, 215); box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 4px; border-radius: 4px; left: -10000px; top: -10000px; z-index: -2147483648; position: absolute; transition: opacity 0.15s ease-out 0s; opacity: 0; visibility: hidden;"
200
+ aria-hidden="true">
201
+ <div style="position: relative; z-index: 1;"><iframe src="Just%20a%20moment_files/hcaptcha_004.html"
202
+ title="Main content of the hCaptcha challenge" scrolling="no"
203
+ style="border: 0px none; z-index: 2000000000; position: relative;" frameborder="0"></iframe></div>
204
+ <div
205
+ style="width: 100%; height: 100%; position: fixed; pointer-events: none; top: 0px; left: 0px; z-index: 0; background-color: rgb(255, 255, 255); opacity: 0.05;">
206
+ </div>
207
+ <div
208
+ style="border-width: 11px; position: absolute; pointer-events: none; margin-top: -11px; z-index: 1; right: 100%;">
209
+ <div
210
+ style="border-width: 10px; border-style: solid; border-color: transparent rgb(255, 255, 255) transparent transparent; position: relative; top: 10px; z-index: 1;">
211
+ </div>
212
+ <div
213
+ style="border-width: 11px; border-style: solid; border-color: transparent rgb(215, 215, 215) transparent transparent; position: relative; top: -11px; z-index: 0;">
214
+ </div>
215
+ </div>
216
+ </div>
217
+ </body>
218
+
219
+ </html>
html_samples/cloudflare_captcha_norobot_v1.html ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+
4
+ <head>
5
+ <title>Just a moment...</title>
6
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
7
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge">
8
+ <meta name="robots" content="noindex,nofollow">
9
+ <meta name="viewport" content="width=device-width,initial-scale=1">
10
+ <link href="Just%20a%20moment2_files/cf-errors.css" rel="stylesheet">
11
+
12
+ <script>
13
+ (function () {
14
+ window._cf_chl_opt = {
15
+ cvId: '2',
16
+ cType: 'managed',
17
+ cNounce: '94250',
18
+ cRay: '732fc1c74f757330',
19
+ cHash: '8c4978fa93c1751',
20
+ cUPMDTk: "\/search?q=2022&__cf_chl_tk=6E3KpS5eCzuCMJG64ch2shvOMHdwQ8ioliqACpoQqM8-1659201542-0-gaNycGzNCeU",
21
+ cFPWv: 'g',
22
+ cTTimeMs: '1000',
23
+ cTplV: 2,
24
+ cRq: {
25
+ ru: 'aHR0cHM6Ly8wbWFnbmV0LmNvbS9zZWFyY2g/cT0yMDIy',
26
+ ra: 'TW96aWxsYS81LjAgKFgxMTsgTGludXggeDg2XzY0OyBydjoxMDUuMCkgR2Vja28vMjAxMDAxMDEgRmlyZWZveC8xMDUuMA==',
27
+ rm: 'R0VU',
28
+ d: 'C4CtJo9JDMtUWZ0r+/s2CwjYdSTdqGYK3qFo1OXpvSc9v7/3d5QuMwmvG3e5oV1BpjlQb8eJJ23gVRxavjw/gpPp1brmKoHuvcJEmAP3Sof38vqcpF91/9NHe3JbmCM2xshiGvJdbpJXb5wXdYKYPMqy7NUHL1VU4hupa3Da3tBq9zyuMa1NcZaiyeE6piSl7n96m+VziRdwyG+SBUldIG/Fsv9J1yl+Gj19wbX1XEneMXChcClGgRrSe1MTd9thLkq2NGFqROnsUmpA8b+2Eqi+IPYQfkPcydWkHmJqQixN9ZFTIBChIC60hGHOQ7O354ju65tVGAhB/nBRREpdqvwoYzgufgg83+dbPHVdQasiuLRHvftOtHhS5/iaBOVoEBH+rElTSk/OYjU2Yh6gkQj0FjkbebEBptFeVAxgqoYZljOrhamWYYZ14tOKeonzc1rz/FXNTM5qVtrWCwAlt9SsXDjM/GYXZMTbOdNLnLZGlLNQCx+l6hMC0OQC45sWFzZECljbjXwiYfodKobeqe11lUXnskj8AN5Qc7O8OqtALsxoNCLZ7ou+ORY0lauremeuu3U3WqadgSGFGA+TZZw2VcCA3BIUKCGlsNLBlJ8wQS2UAGJfGLOVuhErmtsM',
29
+ t: 'MTY1OTIwMTU0Mi4yOTUwMDA=',
30
+ m: 'eWHHJ28v6yOyvSePVqcdyHxAYkkc3xq3VJ8YiDCk5nk=',
31
+ i1: 'M3dMvem+HcwSbNQrJbaYdQ==',
32
+ i2: 'ebY327qYCu6NZKHSQXkbaQ==',
33
+ zh: 'qP4bnGc6j96JlnjNSE7HmQci3S9L50bHFtm4bQRjjKU=',
34
+ uh: 'SK3PXNkeRzZtkRARhJpbmZpCIiWQw6+5gpOE7vojWx4=',
35
+ hh: 'azXzJl8Ou22g0nN/9idVUoB9EqZ7fLmkSdDRHM3Lkmw=',
36
+ }
37
+ }
38
+ window._cf_chl_enter = function () { window._cf_chl_opt.p = 1 };
39
+ })();
40
+ </script>
41
+
42
+ <script src="Just%20a%20moment2_files/v1.js"></script>
43
+ <script type="text/javascript" src="Just%20a%20moment2_files/api.js"></script>
44
+ </head>
45
+
46
+ <body class="no-js">
47
+
48
+ <div class="privacy-pass">
49
+ <a rel="noopener noreferrer" href="https://addons.mozilla.org/en-US/firefox/addon/privacy-pass/"
50
+ target="_blank">
51
+ Privacy Pass
52
+ <span class="privacy-pass-icon-wrapper">
53
+ <div class="privacy-pass-icon"></div>
54
+ </span>
55
+ </a>
56
+ </div>
57
+
58
+ <div class="main-wrapper" role="main">
59
+ <div class="main-content">
60
+ <h1 class="zone-name-title h1">
61
+ <img class="heading-favicon" src="Just%20a%20moment2_files/favicon.ico"
62
+ onerror="this.onerror=null;this.parentNode.removeChild(this)">
63
+ 0MAGNET.COM
64
+ </h1>
65
+ <h2 class="h2" id="cf-challenge-running">
66
+ Checking if the site connection is secure
67
+ </h2>
68
+ <div id="cf-challenge-stage" style="display: block;">
69
+ <div id="cf-norobot-container" style="display: flex;"><input type="button" value="Verify you are human"
70
+ class="big-button pow-button" style="cursor: pointer;"></div>
71
+ </div>
72
+ <div id="cf-challenge-spinner" class="spacer loading-spinner" style="display: none; visibility: hidden;">
73
+ <div class="lds-ring">
74
+ <div></div>
75
+ <div></div>
76
+ <div></div>
77
+ <div></div>
78
+ </div>
79
+ </div>
80
+ <noscript>
81
+ <div id="cf-challenge-error-title">
82
+ <div class="h2">
83
+ <span class="icon-wrapper">
84
+ <div class="heading-icon warning-icon"></div>
85
+ </span>
86
+ <span id="cf-challenge-error-text">
87
+ Enable JavaScript and cookies to continue
88
+ </span>
89
+ </div>
90
+ </div>
91
+ </noscript>
92
+ <div
93
+ style="display:none;background-image:url('/cdn-cgi/images/trace/captcha/nojs/transparent.gif?ray=732fc1c74f757330')">
94
+ </div>
95
+ <div id="cf-challenge-body-text" class="core-msg spacer">
96
+ 0magnet.com needs to review the security of your connection before
97
+ proceeding.
98
+ </div>
99
+ <div id="cf-challenge-fact-wrapper" style="display: block; visibility: visible;" class="fact spacer hidden">
100
+ <span class="fact-title">Did you know</span> <span id="cf-challenge-fact" class="body-text">botnets can
101
+ be used to shutdown popular websites?</span>
102
+ </div>
103
+ <div id="cf-challenge-explainer-expandable" class="hidden expandable body-text spacer"
104
+ style="display: block; visibility: visible;">
105
+ <div class="expandable-title" id="cf-challenge-explainer-summary"><button class="expandable-summary-btn"
106
+ id="cf-challenge-explainer-btn" type="button"> Why am I seeing this page? <span
107
+ class="caret-icon-wrapper">
108
+ <div class="caret-icon"></div>
109
+ </span> </button> </div>
110
+ <div class="expandable-details" id="cf-challenge-explainer-details">
111
+ Requests from malicious bots can pose as legitimate traffic.
112
+ Occasionally, you may see this page while the site ensures that the
113
+ connection is secure.</div>
114
+ </div>
115
+ <div id="cf-challenge-success" style="display: none;">
116
+ <div class="h2"><span class="icon-wrapper"><img class="heading-icon" alt="Success icon"
117
+ src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAMAAADypuvZAAAANlBMVEUAAAAxMTEwMDAxMTExMTEwMDAwMDAwMDAxMTExMTExMTEwMDAwMDAxMTExMTEwMDAwMDAxMTHB9N+uAAAAEXRSTlMA3zDvfyBAEJC/n3BQz69gX7VMkcMAAAGySURBVEjHnZZbFoMgDEQJiDzVuv/NtgbtFGuQ4/zUKpeMIQbUhXSKE5l1XSn4pFWHRm/WShT1HRLWC01LGxFEVkCc30eYkLJ1Sjk9pvkw690VY6k8DWP9OM9yMG0Koi+mi8XA36NXmW0UXra4eJ3iwHfrfXVlgL0NqqGBHdqfeQhMmyJ48WDuKP81h3+SMPeRKkJcSXiLUK4XTHCjESOnz1VUXQoc6lgi2x4cI5aTQ201Mt8wHysI5fc05M5c81uZEtHcMKhxZ7iYEty1GfhLvGKpm+EYkdGxm1F5axmcB93DoORIbXfdN7f+hlFuyxtDP+sxtBnF43cIYwaZAWRgzxIoiXEMESoPlMhwLRDXeK772CAzXEdBRV7cmnoVBp0OSlyGidEzJTFq5hhcsA5388oSGM6b5p+qjpZrBlMS9xj4AwXmz108ukU1IomM3ceiW0CDwHCqp1NjAqXlFrbga+xuloQJ+tuyfbIBPNpqnmxqT7dPaOnZqBfhSBCteJAxWj58zLk2xgg+SPGYM6dRO6WczSnIxxwEExRaO+UyCUhbOp7CGQ+kxSUfNtLQFC+Po29vvy7jj4y0yAAAAABJRU5ErkJggg=="></span>Connection
118
+ is secure</div>
119
+ <div class="core-msg spacer">Proceeding...</div>
120
+ </div>
121
+ <form id="challenge-form"
122
+ action="/search?q=2022&amp;__cf_chl_f_tk=6E3KpS5eCzuCMJG64ch2shvOMHdwQ8ioliqACpoQqM8-1659201542-0-gaNycGzNCeU"
123
+ method="POST" enctype="application/x-www-form-urlencoded">
124
+ <input type="hidden" name="md"
125
+ value="UPeuijc1TS5ZQ21GIY6wjg6HHN_jWKH9sqolcSJABwg-1659201542-0-AR_ZxgiwVB4GwEgAjllIrmnGAumHNwuvfpFBddySYLh6CWexrUnxVYlX_wlB19Yndm45fs-KngMxbYB4dEOuf4MOJ_yL_BsNG3_cIPybV0bNn9WQXecJg3FfFrIBuMFIappZOX4hdDjLtRo9f4JsVsU6FzD9sUoKJRd4BTkjTAm25yFbqmPgV15XZhnJ5HRux044u0IIOVZCwTTzgRLCqToVb-OfiuUcHBzt4W7_wNlF1ObUi2oEr00DA1zZvzzY2KnXdZVN8m2OaNY_f2zkk9uDlLQRob_Ti6MHPNDr4eRkyMqZMZ1XDCxe-9lBkcEfpqtg6_4yac9ZiIEoNdJnJVE6cuNzb59DcBooXAq3IWp6fK4y4UIBStjqOXk4bxQb5yt1COfdPuQ9iLE_7yYOPG_t7n5I-4mjwvG7_U337A17oeEemXHfJkGC88Vm3SQdEHiW96VJuOA_X-rb7p3iOMlLYB5DKJ5DaBoPnP86uAWhoHWE6nrVzeAxeQ1y0uBHYPioJba5Kn9d-e2HsTMuAi7ZgSKuk90ApclIiW3owI4bLc4wxO5cu3ZIz7sZfbdvIKDhf9ESZhpQrITU_4Hgqjz0s3lt-MVeNP_0bz31XSeA--pdiulzUpQWLx1jhC4s7Av6STUb9bmbHpE41283KbbpuzBbmHN1UczNiaaquYZiEXRHKYyEMhKD782nWTJwQA">
126
+ <input type="hidden" name="r"
127
+ value="i1ShtnCs9Zs8QexeFnp6EFtrWs3WbGEVQGXbVfYwpRI-1659201542-0-AbDM6G9qkbgoH+BqDdr1tzCDHr/DU9Sdxelapvp2/FZN6VqYfpDkJGv+HxhBQng6aVktcEobxp2ouOxJZxPQrR6tVFIhOW6uPOAdy5kh2BBJWUHfER13aq8LQ86fvDyRh3AThEHj6bgs2udacfvOrDrHT2j/KHBPePlGKbh8rzDTJBKw0ejUleHk8eKX/BQ1bVULgxT+ZZY721lyn2wrjsde1j1OAsiiCDkVvQ4Rs+Bas7UApD5HeWzyrCu2VFk/Qf+Rk+6spM+StYenQUAKXXrekJoIeNxPf/W9ZRsJfwUoY0JUK2thOWiwQOtw21nVDpiCFB9nhhOsmBzBoRQGjckZyu/O5U7jMIFdS9ThCFC0Kffg0MEr5xTkmgw+CNSwN7AlI9v3GS2XdTFOPXe29b68fZXzYfbm2CjqYhmxomZjCGTAmkzXWaVnMOs9Vl/8VurCUEu8SAt5k9Za/vFrEurX1edXNCviVuTOBSLHjqBiLui9FbufzGLq6BaHYi3WIFA1nMkoxduxbErP+Eqyi8UNvzvmEqUbj2COalXcQzkbHkyyLo33MNHZEi1zhhHjwCm1lp6mm4BRe60kRgTHb8X7oxBpY4vEcMz4jQQdsW15xBPAjsH8m9cj1H2ujpd7kfo8JGTyZ7FcoxOzGOuZr8XRpGkH72HaWYz7M+GIb3BBZ1v2Za7sSrzinNLFjHCCVXq68MqOmZ6RhgeexGoKJzcMHsHvgGXB8CisyyNTtA3OQOujybNUnNzlW7vJ/wDreTHkko6jQ/Lm/X2GnLg85BIg6IeROzt3eInAYsCaNKpST/h5bvSGCyzRoOW46oO8ZzZrV2FI2rEr0xLTIVWzQ//K2iGOCz58RisCfxWiF2n+fzj/5nE/0cjTPzYP68TM5BxB058EO7ZEFbgqhUji8IR9V2ahy7kI9dUhwd2S4IyjL+O6hCNPwjpRohkt93wXUCZDMgNoxi1BIylqqtAxYBfodyjFz8mB8GgcqBaCHN3tI0BINENVfvSJwKniYxL73frTX5KEqniT9GdT15o4F7QLf4S1atwYzF6ezJTYgLf6fOWUZKpaFMSRzEsxmZDmOFZeiss8lj7bKS6drOpkaOYzZiSgp5t5VwLKT0yQ+PDWQmqkpZ5WOa9/ayXLyOCunzk1IUO6VkvgFe0P2LZC9XEZUfwAFakYemej8/SZx0EknoPob1il3MMsbfHNAvcvUJK9xDbdAQ7rz34r4D5zO2aPnmYw1yv9K36z78I2dZpjVT9kpiKFwaOTkuSDUDtcmnhKM1XE+goG/C66G6PsChpGKLCeaDw4Rp7BxlumiSGB4Mp/bs8pTz3gez7pSu1oNodr7Tr1wJvCK8T5nVJ5GRO/tQ+Ff2K2s67udoV0CFtKufJyRsGCEv/0u5sArg3uwtwIz1W0JtAVjhe+J2nUihLa0Gqm7AwcCwfhsLHOhMG28V2NAw19iVq8RuMN7A2kGg5PH6bUeilWUxxZvWyDfyRSJZYMQytwAJdt4gQ++Qnl3mcaSk1N3pSiltVUDpfLcYb5gd35m+mKQWtPnIDlJMAtGoBeqROQPLNDg+LYdI/dnJzIOHjI3J+pTWhbAlF7B7NtccZOHmI9Cl3vS6Fpqs5aSPEDoDENTap6JN1kgm5NszMay9tAm66AcKF95W6QhwgQsyRrwScgRaPUtCx9ZJcbav6T/CAulcBB85MjwAd8+HF1g+UZT9VvChZoxh7NzfMoR53pVbxvW6acO8oVN5ITTP8mNAIisRvWi2KVdi4KqaLjYtLFNN8AMzjAC0vBIaFyGZlIbFsB44MRiMufD64b/66dqeC0l0WrUlUG/DgrnSQr6lgK2gONJKPQZGXoaK0Ga8O8xMOkaFLNaqH5UH5KpHvIQ8nwhuXk/MS/7Gdp1W02OEB4l0hhKFytgWdo9QmCquSatvOjuFyRPa6tV8ceGmuDnQw22bJM9BwzdKlHn/2/mHjCz7gcEA3Hb/CbeP8V8mF1mc5R8HEEdz/rx+BTESmGiRivv+WQYpRKNh77iqbYvCvkduK4b3UErNbvcS10aTt8zDFF/oIwjDpsniJsrIUcC0FdQRs2dqPIfkoSHvs7YGmOjx9QThCAiTkPKUE9C5C4YPY4CWRV3nYAFJrTq0F047PkzDYm0AJMCahWK7Vq/Ra3l3nRHu9yI+P0HiruUbkzLgiEJYnAuUtxvpC/Vj0uhr+A0R9Obs1MHkwtDuMs/ETh3ZymeFtWLj70StkslJxTzKGimZSsqQXRFYGHY6CHqHwIXGrArYNjTty48VIfbfaEu58KQp6roOdFmx90AcK2lV0V5UdyuzDJeH/V5ERAmxWLrXQKWgiDrY4ZqecnRk5XEAVMq/ChPts9gR7xsQK5WsHtQNKLfltkL8YvAoS+jZvxzfUUBg99YSC4J/HzQS+FQAnkDxCgeroahXysNN1bgDASXOrn3NsC3LYpUiZ2AVTLPkj1roR9r65O">
128
+
129
+ <span style="display: none;"><span style="display: none;" class="text-gray-600"
130
+ data-translate="error">error code: 1020</span></span>
131
+ </form>
132
+ </div>
133
+ </div>
134
+ <script>
135
+ (function () {
136
+ var trkjs = document.createElement('img');
137
+ trkjs.setAttribute('src', '/cdn-cgi/images/trace/captcha/js/transparent.gif?ray=732fc1c74f757330');
138
+ trkjs.setAttribute('style', 'display: none');
139
+ document.body.appendChild(trkjs);
140
+ var cpo = document.createElement('script');
141
+ cpo.src = '/cdn-cgi/challenge-platform/h/g/orchestrate/managed/v1?ray=732fc1c74f757330';
142
+ window._cf_chl_opt.cOgUHash = location.hash === '' && location.href.indexOf('#') !== -1 ? '#' : location.hash;
143
+ window._cf_chl_opt.cOgUQuery = location.search === '' && location.href.slice(0, -window._cf_chl_opt.cOgUHash.length).indexOf('?') !== -1 ? '?' : location.search;
144
+ if (window.history && window.history.replaceState) {
145
+ var ogU = location.pathname + window._cf_chl_opt.cOgUQuery + window._cf_chl_opt.cOgUHash;
146
+ history.replaceState(null, null, "\/search?q=2022&__cf_chl_rt_tk=6E3KpS5eCzuCMJG64ch2shvOMHdwQ8ioliqACpoQqM8-1659201542-0-gaNycGzNCeU" + window._cf_chl_opt.cOgUHash);
147
+ cpo.onload = function () {
148
+ history.replaceState(null, null, ogU);
149
+ };
150
+ }
151
+ document.getElementsByTagName('head')[0].appendChild(cpo);
152
+ }());
153
+ </script><img src="Just%20a%20moment2_files/transparent.gif" style="display: none">
154
+
155
+ <div class="footer" role="contentinfo">
156
+ <div class="footer-inner">
157
+ <div class="clearfix diagnostic-wrapper">
158
+ <div class="ray-id">Ray ID: <code>732fc1c74f757330</code></div>
159
+ </div>
160
+ <div class="text-center">
161
+ Performance &amp; security by
162
+ <a rel="noopener noreferrer" href="https://www.cloudflare.com/" target="_blank">Cloudflare</a>
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+
168
+ </body>
169
+
170
+ </html>
html_samples/cloudflare_init_v1.html ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+
4
+ <head>
5
+ <title>Just a moment...</title>
6
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
7
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
8
+ <meta name="robots" content="noindex,nofollow" />
9
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
10
+ <link href="/cdn-cgi/styles/cf-errors.css" rel="stylesheet" />
11
+
12
+ <script>
13
+ (function () {
14
+ window._cf_chl_opt = {
15
+ cvId: '2',
16
+ cType: 'managed',
17
+ cNounce: '46449',
18
+ cRay: '732fd3bc9c1d72de',
19
+ cHash: '8838fcad2a7f56c',
20
+ cUPMDTk: "\/search?q=2022&__cf_chl_tk=y4XnN88eYeUiXmFkQeqEipve1VuK0jJA.G4Hz6xztsM-1659202277-0-gaNycGzNBz0",
21
+ cFPWv: 'g',
22
+ cTTimeMs: '1000',
23
+ cTplV: 2,
24
+ cRq: {
25
+ ru: 'aHR0cHM6Ly8wbWFnbmV0LmNvbS9zZWFyY2g/cT0yMDIy',
26
+ ra: 'Y3VybC83Ljg0LjA=',
27
+ rm: 'R0VU',
28
+ d: '+SdFLvm4kJf8Z9BVci1ZbUOY6ab/Dm5Zzyb0IvscIzmY9PnAAcvPfJ/3TD9YJViBxB/ArnbCQrOUfbSkq4odyaZmW19gm+exRuL8Z3POm1ABs7y6jwMshM19q4Gr3eFY/MUO/IYWuyA2F9q94hRCI6ZNb7dLEh9yh6hORbKRd62pdn59h1xCx8tNdKDtP7VXPXo85nYmJJPLOdXTnII+YxZ03a4isAmBHbi+lGoQN/bCV0K006VmpfPElAfAO9jm45o7pc1NgPQhZSKWpTyI/nHMueH6wacPREzN5RtREoQfKuwYpV++Gq56qr5bAe/SKeF+rI0x7OSqC4HQvrNwbA+kHZzaxgOKeiMFjDxmro/GyC/+sxeZmrxnSIAh4BScjPxEl1FLLkg/6D0JH6HmxoT8N/Jgpi9447Am4WeX+WQxJ9+uDs5WrFIahx7pWrgcZUTRPh+UCu3allJ2Q3cAfwK6BclhES/HhBBbJv0pnR1R2RfKDM/gr1MpLuhaK4mFEO/kSyNUjOnCjOfd+5d7Qb0DZn7sHpF2SVc+zNv5OWSvCRDUcNHjIOV6fq0datVyVWmxD6unPS0MMUFO+ZZNiB4ionrhVCiLrb2FjPQ8tzyCqXg+tnV7WtZ0h4+JuK3rxcaQ8PQy60/As8dKHqVTnw==',
29
+ t: 'MTY1OTIwMjI3Ny44NjMwMDA=',
30
+ m: 'zvAOPvfoONkW1BzH+jMnKOPtDpPpZijRP52DVDWH+i8=',
31
+ i1: 'dDlQDNhOEuHzFEPo/etoAA==',
32
+ i2: '+LTK9hchBRjTTQk1WQU1Vw==',
33
+ zh: 'qP4bnGc6j96JlnjNSE7HmQci3S9L50bHFtm4bQRjjKU=',
34
+ uh: 'IdIU2i4FhVxxcYhzSFWdjoBuQm7qnyVK65JGofJuWV4=',
35
+ hh: 'azXzJl8Ou22g0nN/9idVUoB9EqZ7fLmkSdDRHM3Lkmw=',
36
+ }
37
+ }
38
+ window._cf_chl_enter = function () { window._cf_chl_opt.p = 1 };
39
+ })();
40
+ </script>
41
+
42
+ </head>
43
+
44
+ <body class="no-js">
45
+
46
+ <div class="main-wrapper" role="main">
47
+ <div class="main-content">
48
+ <h1 class="zone-name-title h1">
49
+ <img class="heading-favicon" src="/favicon.ico"
50
+ onerror="this.onerror=null;this.parentNode.removeChild(this)" />
51
+ 0MAGNET.COM
52
+ </h1>
53
+ <h2 class="h2" id="cf-challenge-running">
54
+ Checking if the site connection is secure
55
+ </h2>
56
+ <noscript>
57
+ <div id="cf-challenge-error-title">
58
+ <div class="h2">
59
+ <span class="icon-wrapper">
60
+ <div class="heading-icon warning-icon"></div>
61
+ </span>
62
+ <span id="cf-challenge-error-text">
63
+ Enable JavaScript and cookies to continue
64
+ </span>
65
+ </div>
66
+ </div>
67
+ </noscript>
68
+ <div
69
+ style="display:none;background-image:url('/cdn-cgi/images/trace/captcha/nojs/transparent.gif?ray=732fd3bc9c1d72de')">
70
+ </div>
71
+ <div id="cf-challenge-body-text" class="core-msg spacer">
72
+ 0magnet.com needs to review the security of your connection before
73
+ proceeding.
74
+ </div>
75
+ <form id="challenge-form"
76
+ action="/search?q=2022&amp;__cf_chl_f_tk=y4XnN88eYeUiXmFkQeqEipve1VuK0jJA.G4Hz6xztsM-1659202277-0-gaNycGzNBz0"
77
+ method="POST" enctype="application/x-www-form-urlencoded">
78
+ <input type="hidden" name="md"
79
+ value="DpGhFnuVRfDhqsQNASrgdT4WiiJ8m6lqTIs03.l6RLc-1659202277-0-AfUEAk9DsJ4rmpVI_Al7-eogy2CmM3YgWe4-31iw0oG2CcDIbYvauEW2IvK9m27_gq1FvdH-UPaGHR0q6Q2haXlX4pgmQK5rlQUSEd5HquGdtWMasHWqL_Q_TZGdOKz30bE2FEk8wLHRErHJRJloDRj0tiG8MreT2La_GLvovNK1XbMXDxFZT2Cc-DThBvxbgbDffw3okYfdl1ECXhLw9G6L4o8xgLsz3QZQG3dNZNhm5n4mf55-BBsFDzDTEN1_1BgORVw3mtbsodedktcACsVBCRupyBpTev9MML1jHzk06ZT9dhcCP4zXvsMS4-gG212LFu79Cpl0MHifKvPk0DTJQja1ulaT4gVuIvmLPihPh1IYMGbEcdX4MFH0Wu_RL6UPINE6esf-oAx8-imKhKITB_R4974rpq9XJk65Kf9R6AJhu072CyOqW1YcmYMkUCqFjdZnRyNgHRT2Q5bMEJ8fv0DwfFV6ynG7n6JGMd_pEnZp0nEvjWXpK6Ft8ZZGOXtFMfmW4vNgFhs6xJ1wnaJWuLXae3V6gTZYxMkeIsyMzlvRSzYBz_rgRBNkvvAwbNvOZ369tKbaElS39hOI1WTaoOsnY2d0Z4mDe4AVbSs3fVJGikzZSa3Ctr1RnqqOztVIRYL1Q7IYRJ02P6egL7sn7RniJ6znNAoPhaWJLYzynWXeQF5YO5U0Zf779qkm3A" />
80
+ <input type="hidden" name="r"
81
+ value="QJznOl.RWpNvkdG1Pf6TAzaNhRIFpH8DJ0w1yAuwRLw-1659202277-0-AcOBapBisncM3qf1RYdkTNlIXCth/TmoAMnk3vJozFlG8/vYeLPpjG389mhQu01aSlpJqFWn0VQf9c/7w3yh85jHmrpaxJtpxTiSL9k+AWm61kE6DkHgJBl5jUc7gu4W3oHdmP4FyOUzhbBpIkOAntSkVJJmgu6SaIE3I9fRAFu7bPxBveT8zZGyVUJSPKpwx/w4rNPzs2VnCeEVL1eOdbLInHYR1kqC8M4JyBynwdVXxIX+j5o/rTrNK8E/W4UZMhuWqIaOnX7FzmceglyBSjDqJFLCt0TOhc66m82Y25Obi8Gvsqn34bjwPA2G8qOvgrHA2RFH6lEQFSdGMzLrF4qU5P9j9FzU1CPSTfGtkKbsMnGcMrtzmyQ7LdMIfghYvnCBXTi82iIzaSwzY3sEnW9KZs24Akxu/AV1E03sqW1CAA1UCRURpX4GKXvD6UYpSgc6++q8naLdRozkLP81T/CvHyIRdQx8vylmVN9u/rPvMbW1jWtniDmuAjBQDUd058YH+IRmm4lREG5JN2yeX083h/BG6tssEQVdTcIgwZRNDB+kK8vtOmywmo5qTAX1VE/sgfPCw5+3Xxu+hhZON3C7VGfrCQI5ZSb6+YvBLXmO26Nlp3fSOyeBwZy3pVuGwv/TrEo+e8USIlIs1T6MQJYQeX/4vOdy89npo6KBqY23giTFDh8EMZo//93hfBsRUbHrY/It6kp42qzsnWTjbkyiqd1zBSpQhuMyuMPeKpQ63oVI2tlGyioLg3HcfhbHQcdpUAWDn8lZ4+GTFVMix+20fGbErkVeBs7WvFSLlZ1YtYpCXrgVaomj7WCr8Icb7ASXKfvEuqC1ZnZgn6Lb6x3dUBGiDtnSFnixHFElIF6nPedVIV0+TxccjlV/LJeyNM58GHtRo4NcmIo1a6kN3vzPAjTUhgDJe4aYP6oVKCRNDcrHlGlLubu6XuIvBFM5Sq401xxahOe3VP2u7JovkzXwfl+yUxQOYaoq1LR+wnDhXgVbBNbM2QfIhez578zu2TN5bu5H14UXZ1E78KA6Op9b/PUgA1AsgTJVRk4M6OQSpa5wRIKkXzxpGIRz6+YBxSjIaX2I220GH4s6Te4CBpq77g6V4CVIkEvqZwbN9hIoAoljWVbEEdb3WmYZqoPxN/8ZIjU7uwQUyDgnCOlc7Z52TgG6nVvj7RVyxv5ugskW+fcOI12o35iYNNpXTh1boHyn7nlPG7wtSsl9UlTss27nd04AIzbH0qyX3kn77yPsobMDYUJ3IGhOujV8Cg08XHFIlYSYGPbqqpog+CuuWtzvwyk5mmHXkJNPyFEZL/irApJbatpGNqgGNnQL+5KYp+/U8/kROLTOWa8tG5609MF+wdrScsfPT9eE+HYh7tEFURnwm8kJtdAadcxYjzO60PFcUI1R5SMGHRflAnpY2gvAzbsSssk1WIF+6eHSe6FLHMXCHMp0w1XkNKpny5Ce3YTKhJ4TRg7HfN1pvet2Duj4G04A328uYUppPlU7Spz0fj5N/FHJf3sPaqJC8jn74L0mT92ecGaxS3ZGvytw51ulA00wgzfZDWL4pirzgYVjUQTqVl9FzWYua4Vk4l3BX0opWKA4FloLTP3ekrvmO/zkztMBV4fvK+F8JIOzLOs4AuoCv8uXl7Ny9wLQI3a0hJAdbXJpI3WV/iuV7da4fQao2Z2HiatQh3ZtdLqWmGtqQlcVtsBrac82eo7mKAfwltTfLlX9Drtp4ohwoFe0Upm+YsfY6DK7zHrk3k9GN7gm6cMi1neNFaqWZR9s8ABDBg==" />
82
+
83
+ </form>
84
+ </div>
85
+ </div>
86
+ <script>
87
+ (function () {
88
+ var trkjs = document.createElement('img');
89
+ trkjs.setAttribute('src', '/cdn-cgi/images/trace/captcha/js/transparent.gif?ray=732fd3bc9c1d72de');
90
+ trkjs.setAttribute('style', 'display: none');
91
+ document.body.appendChild(trkjs);
92
+ var cpo = document.createElement('script');
93
+ cpo.src = '/cdn-cgi/challenge-platform/h/g/orchestrate/managed/v1?ray=732fd3bc9c1d72de';
94
+ window._cf_chl_opt.cOgUHash = location.hash === '' && location.href.indexOf('#') !== -1 ? '#' : location.hash;
95
+ window._cf_chl_opt.cOgUQuery = location.search === '' && location.href.slice(0, -window._cf_chl_opt.cOgUHash.length).indexOf('?') !== -1 ? '?' : location.search;
96
+ if (window.history && window.history.replaceState) {
97
+ var ogU = location.pathname + window._cf_chl_opt.cOgUQuery + window._cf_chl_opt.cOgUHash;
98
+ history.replaceState(null, null, "\/search?q=2022&__cf_chl_rt_tk=y4XnN88eYeUiXmFkQeqEipve1VuK0jJA.G4Hz6xztsM-1659202277-0-gaNycGzNBz0" + window._cf_chl_opt.cOgUHash);
99
+ cpo.onload = function () {
100
+ history.replaceState(null, null, ogU);
101
+ };
102
+ }
103
+ document.getElementsByTagName('head')[0].appendChild(cpo);
104
+ }());
105
+ </script>
106
+
107
+ <div class="footer" role="contentinfo">
108
+ <div class="footer-inner">
109
+ <div class="clearfix diagnostic-wrapper">
110
+ <div class="ray-id">Ray ID: <code>732fd3bc9c1d72de</code></div>
111
+ </div>
112
+ <div class="text-center">
113
+ Performance &amp; security by
114
+ <a rel="noopener noreferrer" href="https://www.cloudflare.com" target="_blank">Cloudflare</a>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </body>
119
+
120
+ </html>
html_samples/cloudflare_spinner_v1.html ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <html lang="en-US">
2
+
3
+ <head>
4
+ <title>Just a moment...</title>
5
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
6
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge">
7
+ <meta name="robots" content="noindex,nofollow">
8
+ <meta name="viewport" content="width=device-width,initial-scale=1">
9
+ <link href="/cdn-cgi/styles/cf-errors.css" rel="stylesheet">
10
+
11
+ <script>
12
+ (function () {
13
+ window._cf_chl_opt = {
14
+ cvId: '2',
15
+ cType: 'managed',
16
+ cNounce: '52875',
17
+ cRay: '732fa2449b567521',
18
+ cHash: '79cce74ebb92671',
19
+ cUPMDTk: "\/search?q=2022&__cf_chl_tk=1qWQAgl8.irfEoDb73Rb0pUm1SXbis3ZamDAIoTcPks-1659200251-0-gaNycGzNCFE",
20
+ cFPWv: 'g',
21
+ cTTimeMs: '1000',
22
+ cTplV: 2,
23
+ cRq: {
24
+ ru: 'aHR0cHM6Ly8wbWFnbmV0LmNvbS9zZWFyY2g/cT0yMDIy',
25
+ ra: 'TW96aWxsYS81LjAgKFgxMTsgTGludXggeDg2XzY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvMTAzLjAuNTA2MC4xMzQgU2FmYXJpLzUzNy4zNg==',
26
+ rm: 'R0VU',
27
+ d: 'UfK0k9mFeKGEdqoWAUIbk3OXbXe9DOHoYXdKLPyxbICSIQBS4GSNYar0DtbPI7+UQ7UeBZ2XCdQinvgH0pgzJCF1qB0nkXtu0qlLk6EwkrGAKD/pMGFFQF2EaCw3m00/xoRCDgLZRl/wUkRGz3HUOkTuPeKgZjsFyPoPv7MbYSMUtH7QU6ruIh+O3hvDOT2oA/BOKbRMSTnFedTIXADXL6GE8ZyNZ33wJlef5KzT0MHlN+3eZTAt6urCvJaY3MdTXKVye6fwyjqGEksaJ6B85vwrifLTYEU4/bORwXx8mTQTqjo3kh1rATlmthQwBpcQtWXmgDUcJ5gPrOk1fzhqrhO4b++HiIx3P5YZ9Ko2D0NNWeg1AYIwDjh9rZg5m0MmCXh1VqXDbnpseQW1vPkkZAADxyvLf/eEc1o2EpYGpK+qSpMZ4RcngnU0o8A2nS+j/CNsid0315OrYVyOIZcw6L3ovu6yfAAAALyOmg5ctXCqjzRthoibUb58u+myxOtfX1ew9IzNq8Z6t6RlomjR7Iy/7BJiJQNCF98dllNbODHz//TymlI1m8D9w+CYlZFIpiWJVH1M4h+tabH5YrqDVbkJgY6yVAfnr/NI6d6NHrhN+eSW30jkvAmZ6JRMhVWW',
28
+ t: 'MTY1OTIwMDI1MS42MjAwMDA=',
29
+ m: '/e8nTBb03IHZzN/DSkoHPRu0Ndm3ynYs8g6ZC+VxHcc=',
30
+ i1: 'tx+ntPfeE2Gv81s52vIOlA==',
31
+ i2: 'fpw8a/EO+Fo2t/ZiNKxEcg==',
32
+ zh: 'qP4bnGc6j96JlnjNSE7HmQci3S9L50bHFtm4bQRjjKU=',
33
+ uh: 'Eex9UQDjphKtV6LyVQ95F/MC5kBA3Rj4lC6CudiU3Vs=',
34
+ hh: 'azXzJl8Ou22g0nN/9idVUoB9EqZ7fLmkSdDRHM3Lkmw=',
35
+ }
36
+ }
37
+ window._cf_chl_enter = function () { window._cf_chl_opt.p = 1 };
38
+ })();
39
+ </script>
40
+
41
+ <script src="/cdn-cgi/challenge-platform/h/g/orchestrate/managed/v1?ray=732fa2449b567521"></script>
42
+ <script type="text/javascript"
43
+ src="https://cloudflare.hcaptcha.com/1/api.js?endpoint=https%3A%2F%2Fcloudflare.hcaptcha.com&amp;assethost=https%3A%2F%2Fcf-assets.hcaptcha.com&amp;imghost=https%3A%2F%2Fcf-imgs.hcaptcha.com&amp;render=explicit&amp;recaptchacompat=off&amp;onload=_cf_chl_hload"></script>
44
+ </head>
45
+
46
+ <body class="no-js">
47
+
48
+ <div class="privacy-pass">
49
+ <a rel="noopener noreferrer"
50
+ href="https://chrome.google.com/webstore/detail/privacy-pass/ajhmfdgkijocedmfjonnpjfojldioehi"
51
+ target="_blank">
52
+ Privacy Pass
53
+ <span class="privacy-pass-icon-wrapper">
54
+ <div class="privacy-pass-icon"></div>
55
+ </span>
56
+ </a>
57
+ </div>
58
+
59
+ <div class="main-wrapper" role="main">
60
+ <div class="main-content">
61
+ <h1 class="zone-name-title h1">
62
+ <img class="heading-favicon" src="/favicon.ico"
63
+ onerror="this.onerror=null;this.parentNode.removeChild(this)">
64
+ 0MAGNET.COM
65
+ </h1>
66
+ <h2 class="h2" id="cf-challenge-running">
67
+ Checking if the site connection is secure
68
+ </h2>
69
+ <div id="cf-challenge-stage" style="display: none;"></div>
70
+ <div id="cf-challenge-spinner" class="spacer loading-spinner" style="display: block; visibility: visible;">
71
+ <div class="lds-ring">
72
+ <div></div>
73
+ <div></div>
74
+ <div></div>
75
+ <div></div>
76
+ </div>
77
+ </div>
78
+ <noscript>
79
+ <div id="cf-challenge-error-title">
80
+ <div class="h2">
81
+ <span class="icon-wrapper">
82
+ <div class="heading-icon warning-icon"></div>
83
+ </span>
84
+ <span id="cf-challenge-error-text">
85
+ Enable JavaScript and cookies to continue
86
+ </span>
87
+ </div>
88
+ </div>
89
+ </noscript>
90
+ <div
91
+ style="display:none;background-image:url('/cdn-cgi/images/trace/captcha/nojs/transparent.gif?ray=732fa2449b567521')">
92
+ </div>
93
+ <div id="cf-challenge-body-text" class="core-msg spacer">
94
+ 0magnet.com needs to review the security of your connection before
95
+ proceeding.
96
+ </div>
97
+ <div id="cf-challenge-fact-wrapper" class="fact spacer hidden" style="display: block; visibility: visible;">
98
+ <span class="fact-title">Did you know</span> <span id="cf-challenge-fact" class="body-text">bots
99
+ historically made up nearly 40% of all internet traffic?</span>
100
+ </div>
101
+ <div id="cf-challenge-explainer-expandable" class="hidden expandable body-text spacer"
102
+ style="display: none;">
103
+ <div class="expandable-title" id="cf-challenge-explainer-summary"><button class="expandable-summary-btn"
104
+ id="cf-challenge-explainer-btn" type="button"> Why am I seeing this page? <span
105
+ class="caret-icon-wrapper">
106
+ <div class="caret-icon"></div>
107
+ </span> </button> </div>
108
+ <div class="expandable-details" id="cf-challenge-explainer-details"> Requests from malicious bots can
109
+ pose as legitimate traffic. Occasionally, you may see this page while the site ensures that the
110
+ connection is secure.</div>
111
+ </div>
112
+ <div id="cf-challenge-success" style="display: none;">
113
+ <div class="h2"><span class="icon-wrapper"><img class="heading-icon" alt="Success icon"
114
+ src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAMAAADypuvZAAAANlBMVEUAAAAxMTEwMDAxMTExMTEwMDAwMDAwMDAxMTExMTExMTEwMDAwMDAxMTExMTEwMDAwMDAxMTHB9N+uAAAAEXRSTlMA3zDvfyBAEJC/n3BQz69gX7VMkcMAAAGySURBVEjHnZZbFoMgDEQJiDzVuv/NtgbtFGuQ4/zUKpeMIQbUhXSKE5l1XSn4pFWHRm/WShT1HRLWC01LGxFEVkCc30eYkLJ1Sjk9pvkw690VY6k8DWP9OM9yMG0Koi+mi8XA36NXmW0UXra4eJ3iwHfrfXVlgL0NqqGBHdqfeQhMmyJ48WDuKP81h3+SMPeRKkJcSXiLUK4XTHCjESOnz1VUXQoc6lgi2x4cI5aTQ201Mt8wHysI5fc05M5c81uZEtHcMKhxZ7iYEty1GfhLvGKpm+EYkdGxm1F5axmcB93DoORIbXfdN7f+hlFuyxtDP+sxtBnF43cIYwaZAWRgzxIoiXEMESoPlMhwLRDXeK772CAzXEdBRV7cmnoVBp0OSlyGidEzJTFq5hhcsA5388oSGM6b5p+qjpZrBlMS9xj4AwXmz108ukU1IomM3ceiW0CDwHCqp1NjAqXlFrbga+xuloQJ+tuyfbIBPNpqnmxqT7dPaOnZqBfhSBCteJAxWj58zLk2xgg+SPGYM6dRO6WczSnIxxwEExRaO+UyCUhbOp7CGQ+kxSUfNtLQFC+Po29vvy7jj4y0yAAAAABJRU5ErkJggg=="></span>Connection
115
+ is secure</div>
116
+ <div class="core-msg spacer">Proceeding...</div>
117
+ </div>
118
+ <form id="challenge-form"
119
+ action="/search?q=2022&amp;__cf_chl_f_tk=1qWQAgl8.irfEoDb73Rb0pUm1SXbis3ZamDAIoTcPks-1659200251-0-gaNycGzNCFE"
120
+ method="POST" enctype="application/x-www-form-urlencoded">
121
+ <input type="hidden" name="md"
122
+ value="OghUU_ltYW6I0fpWl7rE4yHBGBPHfpZQIKZRSEpJKjE-1659200251-0-AWB-KR-MabhObmvYa3mR5-xDk3qZVV73547wjnl-QtfPoTxe017AXt4WUskEcVzEIUKC7dsJoiy8ec1NA0fxdnI8X9OfPhtynl00ReWBVZc_3Gba_wigWMmM_9e8PX9vpVDcXpCRbz1BJ5_YLsba9TJM1sp14U9RtIce-tRBB53qoxLxJRz9QFmckEVBvsba4RfoycOvYPMMsfAqSkq13qtsA3Kd6RDB5Rb5-qF8674DsB4AMvd9xu_fBplQqKjOpEtrThCUtw8M2DHY8FUr_owUo1NIS1s6fSBEyHh6ehz9CidJ7zpRwYZFwgz_Pq9i8LmQG_AajozOJJhLp-tox0dptbUZnRNGt3hGQgrNu3jlCfwPC2XVp7xgLvmZoPYrzzrZoi_wErnIvVgyGCw9-sDPblPdvLBUz6uXNreWwThEW6PeRtMXnePO9UwcZmj_2awhwcVSHSLz1t1z22LtVsQ8xNpMbiE7xDvI2D5LNHAPIUC7Wp4AcehWD-fEm0w5jnVTWOmFlVRxtcnYZSMfDSaRUxsZ3hg5B1-ghVMEX6M-r_hAd6pLKNmjIfdl_Nvdm6veQvV-gTFaULbfuhmQQjYEb9G2IptDiNTZs5S7FtmjqVBAA7PmvwBTQwxw86J0cV3v_4pT1Oj8tigwiPny35HMTrKRmRZWaAZudCmWxDZkJIW8Eir7KQ57ba-u9cHh0A">
123
+ <input type="hidden" name="r"
124
+ value="q.UUtPBFcFi4IkcVw3l4U_xJJKDIbHJj7xmuB43IIAI-1659200251-0-Aa4lU5RipD+d4of3hcdQ0rVmZ4ulb3siZYKwhm1jGNiA+/9b8IW1HL8k1GrsYEVexDW7ycP5UINQZ1sYJvZBTCQe3lhyGLHdLZ7KdI9RXKEbPx1NUOR/HthCD0Wbo7H41jbAf7l+HhH0zTLjm77/6NpJZHcgfsbBwwubl4R3oLarzPSByV2PVBnkuMyKCYgibriuMUt2iJHoMLx7Cr+Bmjx1KEFCrPYP0t7vgQs2APTylhL7ebP77XB9ndxU6Of3r4eHnTwLIcomFJ3+jqL6pzFaNoXdUBHrv9oZs/33KZjf2NB8cu5KUpAdM2lp3t5oTSQE19fJVroxmf91hcTdele3F2DAeawFGDwncm/Jo725SlyNk4TqsmR+il7DLkS/FTcCNzQe4cQM6DRWdmF9I1OohAl1/uGXYqUJSK1F45n3gec/pyPTQyZI0OLc7sCGYXfn3VPFsGATkg5mxE9rgZIB2b6ID9JggzIlDdYxlQRWecpruu07KOgk3m7g95lyHNZTohqemo4T8Z2MOZECjmXGMAuwvvk4d5sakVHr39kmAY6aSfXrRB+iONCOKkahbumrVmjLsnMvrpTb0DFE5pRAxwANPZKzb6Ikmlvxh7oJIPOB0mG9hDeoc/AJVlvZJV4CrpDLulNjHetAWXMwMptZuYJGEhcDXxmYj0ybntTCU4Y3JJQc5K+7ehSdnluTvMueWfs628854r4PcOONZzsO337j+3lUxrP5vDUCzYD25FNxvs8jGfqRivqHMOq2z9iOs0sHQTlHroLLSt2G7M50yRJBGTfxrIsvLq+ML3e/mRIkYIQxOcp8ugoPoT4c9gex3OyY0cnnA2/9OibQs9kevwf9DSnutMRRcbIXZI0XO6FY07+MykWqUcXygwMHs1vQxhaQ26NFYwolEWfOL7EQpp4GKyN30nL4nPNil/7GsXIr5SC+o55KI0l3AOEYE1jirVx2G0U7Br7SW80Ih4Fn5U/+4qFfW57GAJrpuk9qjFfJehe7wFBu5bHghEGRhKAu0wvpY7UTc9AiacMfP7ujVWi4DIbTCfOzOgVT8E0T6KaUurBppPJflLQE41c8n29ULyKmki9t8lIKvxYmv/3/AauhXFAExh+JrdnaeSDxhJYjWEUDJiaNvnkDCHMxPs/bePhSg4DYRMh4ngcOHCRkkRlDjipUUgeCrwNBY0qu2DIqLZXI1ZMwU+R0nuWnwc5xJuMHtrLkWbziP0FQcGaF0B6SaFIcOLnWG7YjJZFzxjFpvLb8GnZxk7i2YHCDTn0Stq3JDZHCkjJQjaPMmuK+5KYzfaSHcKOcaQbkbyDjn3t/XQX3a7lknngVchIJVsVn8osqgKvOx3aAdCicYKR6QaukrXHhR9uIEbPdoBYqZPKFz0uvVOShsUx2f65CaI8wWMjOBRWxTK1xUPNsetOiyYSvNwjeULaCXPKLi2qv/cZRRbsr3g5ghdHvNTpD/O0/xUgiziev3/9CpNopyr6VzLar9dJ/s++imXY1w1TCRJ2uCI2H70XGBGWxSZdbnfxU+j3zNCL0dBuabwhDd4ZnOZmlFZjGBiOUpsWdRrHd3c+QpwXdxB3QurRwX6J+LhmkqcWsPhP7LlMnN7dr2HUFZ5FS4LASl5AOf8hjCnO06FT8fWLl1eKVVjCugx9w54qjGqOV8A0v/PdWr7Ic0WfriyYbmwn/XnH8t0ri3bqDZsDkfhQMMF9JSWHEdoGD60a7McGDxr4g9s3LZhq5KozgSvyG+RUBPla8g2zB253hR7amWE5WO4IChl7AXmRB89F9u2+AoDIbefseb3pwG7GfkYpSBwmgJ4Ju4LAWoSfBhZSPMQadHZOCg36R11KesUy+NAy9bvD1bE3UMx9e2NbFohu6sXlilpnxINHp0sFEeulreEjWSQreri1eZeKxV2QfKIzWUiMoNdyT0JzM+/brYzddBpO2DrlnK5bEPWgtu0D7d4Kfm+0T7S//Fq+hxf40lSMPP8cBlan6sEd2iWmZ6gW3z43wNbJaPQIUDgb58ELxaEKQN4tOOy75/XXfISNnhG0K8M79a175WUb8v0A=">
125
+
126
+ <span style="display: none;"><span class="text-gray-600" data-translate="error">error code:
127
+ 1020</span></span>
128
+ </form>
129
+ </div>
130
+ </div>
131
+ <script>
132
+ (function () {
133
+ var trkjs = document.createElement('img');
134
+ trkjs.setAttribute('src', '/cdn-cgi/images/trace/captcha/js/transparent.gif?ray=732fa2449b567521');
135
+ trkjs.setAttribute('style', 'display: none');
136
+ document.body.appendChild(trkjs);
137
+ var cpo = document.createElement('script');
138
+ cpo.src = '/cdn-cgi/challenge-platform/h/g/orchestrate/managed/v1?ray=732fa2449b567521';
139
+ window._cf_chl_opt.cOgUHash = location.hash === '' && location.href.indexOf('#') !== -1 ? '#' : location.hash;
140
+ window._cf_chl_opt.cOgUQuery = location.search === '' && location.href.slice(0, -window._cf_chl_opt.cOgUHash.length).indexOf('?') !== -1 ? '?' : location.search;
141
+ if (window.history && window.history.replaceState) {
142
+ var ogU = location.pathname + window._cf_chl_opt.cOgUQuery + window._cf_chl_opt.cOgUHash;
143
+ history.replaceState(null, null, "\/search?q=2022&__cf_chl_rt_tk=1qWQAgl8.irfEoDb73Rb0pUm1SXbis3ZamDAIoTcPks-1659200251-0-gaNycGzNCFE" + window._cf_chl_opt.cOgUHash);
144
+ cpo.onload = function () {
145
+ history.replaceState(null, null, ogU);
146
+ };
147
+ }
148
+ document.getElementsByTagName('head')[0].appendChild(cpo);
149
+ }());
150
+ </script><img src="/cdn-cgi/images/trace/captcha/js/transparent.gif?ray=732fa2449b567521" style="display: none">
151
+
152
+ <div class="footer" role="contentinfo">
153
+ <div class="footer-inner">
154
+ <div class="clearfix diagnostic-wrapper">
155
+ <div class="ray-id">Ray ID: <code>732fa2449b567521</code></div>
156
+ </div>
157
+ <div class="text-center">
158
+ Performance &amp; security by
159
+ <a rel="noopener noreferrer" href="https://www.cloudflare.com" target="_blank">Cloudflare</a>
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+
165
+ </body>
166
+
167
+ </html>
package.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "flaresolverr",
3
+ "version": "3.4.6",
4
+ "description": "Proxy server to bypass Cloudflare protection",
5
+ "author": "Diego Heras (ngosang / ngosang@hotmail.es)",
6
+ "license": "MIT"
7
+ }
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ bottle==0.13.4
2
+ waitress==3.0.2
3
+ selenium==4.39.0
4
+ func-timeout==4.3.5
5
+ prometheus-client==0.23.1
6
+ # Required by undetected_chromedriver
7
+ requests==2.32.5
8
+ certifi==2025.11.12
9
+ websockets==15.0.1
10
+ packaging==25.0
11
+ # Only required for Linux and macOS
12
+ xvfbwrapper==0.2.16; platform_system != "Windows"
13
+ # Only required for Windows
14
+ pefile==2024.8.26; platform_system == "Windows"
resources/flaresolverr_logo.ico ADDED
resources/flaresolverr_logo.png ADDED
resources/flaresolverr_logo.svg ADDED
src/bottle_plugins/__init__.py ADDED
File without changes
src/bottle_plugins/error_plugin.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from bottle import response
2
+ import logging
3
+
4
+
5
+ def error_plugin(callback):
6
+ """
7
+ Bottle plugin to handle exceptions
8
+ https://stackoverflow.com/a/32764250
9
+ """
10
+
11
+ def wrapper(*args, **kwargs):
12
+ try:
13
+ actual_response = callback(*args, **kwargs)
14
+ except Exception as e:
15
+ logging.error(str(e))
16
+ actual_response = {
17
+ "error": str(e)
18
+ }
19
+ response.status = 500
20
+ return actual_response
21
+
22
+ return wrapper
src/bottle_plugins/logger_plugin.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from bottle import request, response
2
+ import logging
3
+
4
+
5
+ def logger_plugin(callback):
6
+ """
7
+ Bottle plugin to use logging module
8
+ https://bottlepy.org/docs/dev/plugindev.html
9
+
10
+ Wrap a Bottle request so that a log line is emitted after it's handled.
11
+ (This decorator can be extended to take the desired logger as a param.)
12
+ """
13
+
14
+ def wrapper(*args, **kwargs):
15
+ actual_response = callback(*args, **kwargs)
16
+ if not request.url.endswith("/health"):
17
+ logging.info('%s %s %s %s' % (request.remote_addr,
18
+ request.method,
19
+ request.url,
20
+ response.status))
21
+ return actual_response
22
+
23
+ return wrapper
src/bottle_plugins/prometheus_plugin.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import urllib.parse
4
+
5
+ from bottle import request
6
+ from dtos import V1RequestBase, V1ResponseBase
7
+ from metrics import start_metrics_http_server, REQUEST_COUNTER, REQUEST_DURATION
8
+
9
+ PROMETHEUS_ENABLED = os.environ.get('PROMETHEUS_ENABLED', 'false').lower() == 'true'
10
+ PROMETHEUS_PORT = int(os.environ.get('PROMETHEUS_PORT', 8192))
11
+
12
+
13
+ def setup():
14
+ if PROMETHEUS_ENABLED:
15
+ start_metrics_http_server(PROMETHEUS_PORT)
16
+
17
+
18
+ def prometheus_plugin(callback):
19
+ """
20
+ Bottle plugin to expose Prometheus metrics
21
+ https://bottlepy.org/docs/dev/plugindev.html
22
+ """
23
+ def wrapper(*args, **kwargs):
24
+ actual_response = callback(*args, **kwargs)
25
+
26
+ if PROMETHEUS_ENABLED:
27
+ try:
28
+ export_metrics(actual_response)
29
+ except Exception as e:
30
+ logging.warning("Error exporting metrics: " + str(e))
31
+
32
+ return actual_response
33
+
34
+ def export_metrics(actual_response):
35
+ res = V1ResponseBase(actual_response)
36
+
37
+ if res.startTimestamp is None or res.endTimestamp is None:
38
+ # skip management and healthcheck endpoints
39
+ return
40
+
41
+ domain = "unknown"
42
+ if res.solution and res.solution.url:
43
+ domain = parse_domain_url(res.solution.url)
44
+ else:
45
+ # timeout error
46
+ req = V1RequestBase(request.json)
47
+ if req.url:
48
+ domain = parse_domain_url(req.url)
49
+
50
+ run_time = (res.endTimestamp - res.startTimestamp) / 1000
51
+ REQUEST_DURATION.labels(domain=domain).observe(run_time)
52
+
53
+ result = "unknown"
54
+ if res.message == "Challenge solved!":
55
+ result = "solved"
56
+ elif res.message == "Challenge not detected!":
57
+ result = "not_detected"
58
+ elif res.message.startswith("Error"):
59
+ result = "error"
60
+ REQUEST_COUNTER.labels(domain=domain, result=result).inc()
61
+
62
+ def parse_domain_url(url):
63
+ parsed_url = urllib.parse.urlparse(url)
64
+ return parsed_url.hostname
65
+
66
+ return wrapper
src/build_package.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import platform
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ import zipfile
7
+ import tarfile
8
+
9
+ import requests
10
+
11
+
12
+ def clean_files():
13
+ try:
14
+ shutil.rmtree(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 'build'))
15
+ except Exception:
16
+ pass
17
+ try:
18
+ shutil.rmtree(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 'dist'))
19
+ except Exception:
20
+ pass
21
+ try:
22
+ shutil.rmtree(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 'dist_chrome'))
23
+ except Exception:
24
+ pass
25
+
26
+
27
+ def download_chromium():
28
+ # https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Linux_x64/
29
+ revision = "1522586" if os.name == 'nt' else '1522586'
30
+ arch = 'Win_x64' if os.name == 'nt' else 'Linux_x64'
31
+ dl_file = 'chrome-win' if os.name == 'nt' else 'chrome-linux'
32
+ dl_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 'dist_chrome')
33
+ dl_path_folder = os.path.join(dl_path, dl_file)
34
+ dl_path_zip = dl_path_folder + '.zip'
35
+
36
+ # response = requests.get(
37
+ # f'https://commondatastorage.googleapis.com/chromium-browser-snapshots/{arch}/LAST_CHANGE',
38
+ # timeout=30)
39
+ # revision = response.text.strip()
40
+ print("Downloading revision: " + revision)
41
+
42
+ os.mkdir(dl_path)
43
+ with requests.get(
44
+ f'https://commondatastorage.googleapis.com/chromium-browser-snapshots/{arch}/{revision}/{dl_file}.zip',
45
+ stream=True) as r:
46
+ r.raise_for_status()
47
+ with open(dl_path_zip, 'wb') as f:
48
+ for chunk in r.iter_content(chunk_size=8192):
49
+ f.write(chunk)
50
+ print("File downloaded: " + dl_path_zip)
51
+ with zipfile.ZipFile(dl_path_zip, 'r') as zip_ref:
52
+ zip_ref.extractall(dl_path)
53
+ os.remove(dl_path_zip)
54
+
55
+ chrome_path = os.path.join(dl_path, "chrome")
56
+ shutil.move(dl_path_folder, chrome_path)
57
+ print("Extracted in: " + chrome_path)
58
+
59
+ if os.name != 'nt':
60
+ # Give executable permissions for *nix
61
+ # file * | grep executable | cut -d: -f1
62
+ print("Giving executable permissions...")
63
+ execs = ['chrome', 'chrome_crashpad_handler', 'chrome_sandbox', 'chrome-wrapper', 'xdg-mime', 'xdg-settings']
64
+ for exec_file in execs:
65
+ exec_path = os.path.join(chrome_path, exec_file)
66
+ os.chmod(exec_path, 0o755)
67
+
68
+
69
+ def run_pyinstaller():
70
+ sep = ';' if os.name == 'nt' else ':'
71
+ result = subprocess.run([sys.executable, "-m", "PyInstaller",
72
+ "--icon", "resources/flaresolverr_logo.ico",
73
+ "--add-data", f"package.json{sep}.",
74
+ "--add-data", f"{os.path.join('dist_chrome', 'chrome')}{sep}chrome",
75
+ os.path.join("src", "flaresolverr.py")],
76
+ cwd=os.pardir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
77
+ if result.returncode != 0:
78
+ print(result.stderr.decode('utf-8'))
79
+ raise Exception("Error running pyInstaller")
80
+
81
+
82
+ def compress_package():
83
+ dist_folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 'dist')
84
+ package_folder = os.path.join(dist_folder, 'package')
85
+ shutil.move(os.path.join(dist_folder, 'flaresolverr'), os.path.join(package_folder, 'flaresolverr'))
86
+ print("Package folder: " + package_folder)
87
+
88
+ compr_format = 'zip' if os.name == 'nt' else 'gztar'
89
+ compr_file_name = 'flaresolverr_windows_x64' if os.name == 'nt' else 'flaresolverr_linux_x64'
90
+ compr_file_path = os.path.join(dist_folder, compr_file_name)
91
+
92
+ if compr_format == 'zip':
93
+ shutil.make_archive(compr_file_path, compr_format, package_folder)
94
+ print("Compressed file path: " + compr_file_path)
95
+ else:
96
+ def _reset_tarinfo(tarinfo):
97
+ tarinfo.uid = 0
98
+ tarinfo.gid = 0
99
+ tarinfo.uname = ""
100
+ tarinfo.gname = ""
101
+ return tarinfo
102
+
103
+ tar_path = compr_file_path + '.tar.gz'
104
+ with tarfile.open(tar_path, 'w:gz') as tar:
105
+ for entry in os.listdir(package_folder):
106
+ fullpath = os.path.join(package_folder, entry)
107
+ tar.add(fullpath, arcname=entry, filter=_reset_tarinfo)
108
+ print("Compressed file path: " + tar_path)
109
+
110
+ if __name__ == "__main__":
111
+ print("Building package...")
112
+ print("Platform: " + platform.platform())
113
+
114
+ print("Cleaning previous build...")
115
+ clean_files()
116
+
117
+ print("Downloading Chromium...")
118
+ download_chromium()
119
+
120
+ print("Building pyinstaller executable... ")
121
+ run_pyinstaller()
122
+
123
+ print("Compressing package... ")
124
+ compress_package()
125
+
126
+ # NOTE: python -m pip install pyinstaller
src/dtos.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ STATUS_OK = "ok"
3
+ STATUS_ERROR = "error"
4
+
5
+
6
+ class ChallengeResolutionResultT:
7
+ url: str = None
8
+ status: int = None
9
+ headers: list = None
10
+ response: str = None
11
+ cookies: list = None
12
+ userAgent: str = None
13
+ screenshot: str | None = None
14
+ turnstile_token: str = None
15
+
16
+ def __init__(self, _dict):
17
+ self.__dict__.update(_dict)
18
+
19
+
20
+ class ChallengeResolutionT:
21
+ status: str = None
22
+ message: str = None
23
+ result: ChallengeResolutionResultT = None
24
+
25
+ def __init__(self, _dict):
26
+ self.__dict__.update(_dict)
27
+ if self.result is not None:
28
+ self.result = ChallengeResolutionResultT(self.result)
29
+
30
+
31
+ class V1RequestBase(object):
32
+ # V1RequestBase
33
+ cmd: str = None
34
+ cookies: list = None
35
+ maxTimeout: int = None
36
+ proxy: dict = None
37
+ session: str = None
38
+ session_ttl_minutes: int = None
39
+ headers: list = None # deprecated v2.0.0, not used
40
+ userAgent: str = None # deprecated v2.0.0, not used
41
+
42
+ # V1Request
43
+ url: str = None
44
+ postData: str = None
45
+ returnOnlyCookies: bool = None
46
+ returnScreenshot: bool = None
47
+ download: bool = None # deprecated v2.0.0, not used
48
+ returnRawHtml: bool = None # deprecated v2.0.0, not used
49
+ waitInSeconds: int = None
50
+ # Optional resource blocking flag (blocks images, CSS, and fonts)
51
+ disableMedia: bool = None
52
+ # Optional when you've got a turnstile captcha that needs to be clicked after X number of Tab presses
53
+ tabs_till_verify : int = None
54
+
55
+ def __init__(self, _dict):
56
+ self.__dict__.update(_dict)
57
+
58
+
59
+ class V1ResponseBase(object):
60
+ # V1ResponseBase
61
+ status: str = None
62
+ message: str = None
63
+ session: str = None
64
+ sessions: list[str] = None
65
+ startTimestamp: int = None
66
+ endTimestamp: int = None
67
+ version: str = None
68
+
69
+ # V1ResponseSolution
70
+ solution: ChallengeResolutionResultT = None
71
+
72
+ # hidden vars
73
+ __error_500__: bool = False
74
+
75
+ def __init__(self, _dict):
76
+ self.__dict__.update(_dict)
77
+ if self.solution is not None:
78
+ self.solution = ChallengeResolutionResultT(self.solution)
79
+
80
+
81
+ class IndexResponse(object):
82
+ msg: str = None
83
+ version: str = None
84
+ userAgent: str = None
85
+
86
+ def __init__(self, _dict):
87
+ self.__dict__.update(_dict)
88
+
89
+
90
+ class HealthResponse(object):
91
+ status: str = None
92
+
93
+ def __init__(self, _dict):
94
+ self.__dict__.update(_dict)
src/flaresolverr.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import os
4
+ import sys
5
+
6
+ import certifi
7
+ from bottle import run, response, Bottle, request, ServerAdapter
8
+
9
+ from bottle_plugins.error_plugin import error_plugin
10
+ from bottle_plugins.logger_plugin import logger_plugin
11
+ from bottle_plugins import prometheus_plugin
12
+ from dtos import V1RequestBase
13
+ import flaresolverr_service
14
+ import utils
15
+
16
+ env_proxy_url = os.environ.get('PROXY_URL', None)
17
+ env_proxy_username = os.environ.get('PROXY_USERNAME', None)
18
+ env_proxy_password = os.environ.get('PROXY_PASSWORD', None)
19
+
20
+
21
+ class JSONErrorBottle(Bottle):
22
+ """
23
+ Handle 404 errors
24
+ """
25
+ def default_error_handler(self, res):
26
+ response.content_type = 'application/json'
27
+ return json.dumps(dict(error=res.body, status_code=res.status_code))
28
+
29
+
30
+ app = JSONErrorBottle()
31
+
32
+
33
+ @app.route('/')
34
+ def index():
35
+ """
36
+ Show welcome message
37
+ """
38
+ res = flaresolverr_service.index_endpoint()
39
+ return utils.object_to_dict(res)
40
+
41
+
42
+ @app.route('/health')
43
+ def health():
44
+ """
45
+ Healthcheck endpoint.
46
+ This endpoint is special because it doesn't print traces
47
+ """
48
+ res = flaresolverr_service.health_endpoint()
49
+ return utils.object_to_dict(res)
50
+
51
+
52
+ @app.post('/v1')
53
+ def controller_v1():
54
+ """
55
+ Controller v1
56
+ """
57
+ data = request.json or {}
58
+ 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)):
59
+ logging.info('Using proxy URL ENV')
60
+ data['proxy'] = {"url": env_proxy_url}
61
+ 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)):
62
+ logging.info('Using proxy URL, username & password ENVs')
63
+ data['proxy'] = {"url": env_proxy_url, "username": env_proxy_username, "password": env_proxy_password}
64
+ req = V1RequestBase(data)
65
+ res = flaresolverr_service.controller_v1_endpoint(req)
66
+ if res.__error_500__:
67
+ response.status = 500
68
+ return utils.object_to_dict(res)
69
+
70
+
71
+ if __name__ == "__main__":
72
+ # check python version
73
+ if sys.version_info < (3, 9):
74
+ raise Exception("The Python version is less than 3.9, a version equal to or higher is required.")
75
+
76
+ # fix for HEADLESS=false in Windows binary
77
+ # https://stackoverflow.com/a/27694505
78
+ if os.name == 'nt':
79
+ import multiprocessing
80
+ multiprocessing.freeze_support()
81
+
82
+ # fix ssl certificates for compiled binaries
83
+ # https://github.com/pyinstaller/pyinstaller/issues/7229
84
+ # https://stackoverflow.com/q/55736855
85
+ os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
86
+ os.environ["SSL_CERT_FILE"] = certifi.where()
87
+
88
+ # validate configuration
89
+ log_level = os.environ.get('LOG_LEVEL', 'info').upper()
90
+ log_file = os.environ.get('LOG_FILE', None)
91
+ log_html = utils.get_config_log_html()
92
+ headless = utils.get_config_headless()
93
+ server_host = os.environ.get('HOST', '0.0.0.0')
94
+ server_port = int(os.environ.get('PORT', 8191))
95
+
96
+ # configure logger
97
+ logger_format = '%(asctime)s %(levelname)-8s %(message)s'
98
+ if log_level == 'DEBUG':
99
+ logger_format = '%(asctime)s %(levelname)-8s ReqId %(thread)s %(message)s'
100
+ if log_file:
101
+ log_file = os.path.realpath(log_file)
102
+ log_path = os.path.dirname(log_file)
103
+ os.makedirs(log_path, exist_ok=True)
104
+ logging.basicConfig(
105
+ format=logger_format,
106
+ level=log_level,
107
+ datefmt='%Y-%m-%d %H:%M:%S',
108
+ handlers=[
109
+ logging.StreamHandler(sys.stdout),
110
+ logging.FileHandler(log_file)
111
+ ]
112
+ )
113
+ else:
114
+ logging.basicConfig(
115
+ format=logger_format,
116
+ level=log_level,
117
+ datefmt='%Y-%m-%d %H:%M:%S',
118
+ handlers=[
119
+ logging.StreamHandler(sys.stdout)
120
+ ]
121
+ )
122
+
123
+ # disable warning traces from urllib3
124
+ logging.getLogger('urllib3').setLevel(logging.ERROR)
125
+ logging.getLogger('selenium.webdriver.remote.remote_connection').setLevel(logging.WARNING)
126
+ logging.getLogger('undetected_chromedriver').setLevel(logging.WARNING)
127
+
128
+ logging.info(f'FlareSolverr {utils.get_flaresolverr_version()}')
129
+ logging.debug('Debug log enabled')
130
+
131
+ # Get current OS for global variable
132
+ utils.get_current_platform()
133
+
134
+ # test browser installation
135
+ flaresolverr_service.test_browser_installation()
136
+
137
+ # start bootle plugins
138
+ # plugin order is important
139
+ app.install(logger_plugin)
140
+ app.install(error_plugin)
141
+ prometheus_plugin.setup()
142
+ app.install(prometheus_plugin.prometheus_plugin)
143
+
144
+ # start webserver
145
+ # default server 'wsgiref' does not support concurrent requests
146
+ # https://github.com/FlareSolverr/FlareSolverr/issues/680
147
+ # https://github.com/Pylons/waitress/issues/31
148
+ class WaitressServerPoll(ServerAdapter):
149
+ def run(self, handler):
150
+ from waitress import serve
151
+ serve(handler, host=self.host, port=self.port, asyncore_use_poll=True)
152
+ run(app, host=server_host, port=server_port, quiet=True, server=WaitressServerPoll)
src/flaresolverr_service.py ADDED
@@ -0,0 +1,519 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import platform
3
+ import sys
4
+ import time
5
+ from datetime import timedelta
6
+ from html import escape
7
+ from urllib.parse import unquote, quote
8
+
9
+ from func_timeout import FunctionTimedOut, func_timeout
10
+ from selenium.common import TimeoutException
11
+ from selenium.webdriver.chrome.webdriver import WebDriver
12
+ from selenium.webdriver.common.by import By
13
+ from selenium.webdriver.common.keys import Keys
14
+ from selenium.webdriver.support.expected_conditions import (
15
+ presence_of_element_located, staleness_of, title_is)
16
+ from selenium.webdriver.common.action_chains import ActionChains
17
+ from selenium.webdriver.support.wait import WebDriverWait
18
+
19
+ import utils
20
+ from dtos import (STATUS_ERROR, STATUS_OK, ChallengeResolutionResultT,
21
+ ChallengeResolutionT, HealthResponse, IndexResponse,
22
+ V1RequestBase, V1ResponseBase)
23
+ from sessions import SessionsStorage
24
+
25
+ ACCESS_DENIED_TITLES = [
26
+ # Cloudflare
27
+ 'Access denied',
28
+ # Cloudflare http://bitturk.net/ Firefox
29
+ 'Attention Required! | Cloudflare'
30
+ ]
31
+ ACCESS_DENIED_SELECTORS = [
32
+ # Cloudflare
33
+ 'div.cf-error-title span.cf-code-label span',
34
+ # Cloudflare http://bitturk.net/ Firefox
35
+ '#cf-error-details div.cf-error-overview h1'
36
+ ]
37
+ CHALLENGE_TITLES = [
38
+ # Cloudflare
39
+ 'Just a moment...',
40
+ # DDoS-GUARD
41
+ 'DDoS-Guard'
42
+ ]
43
+ CHALLENGE_SELECTORS = [
44
+ # Cloudflare
45
+ '#cf-challenge-running', '.ray_id', '.attack-box', '#cf-please-wait', '#challenge-spinner', '#trk_jschal_js', '#turnstile-wrapper', '.lds-ring',
46
+ # Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
47
+ 'td.info #js_info',
48
+ # Fairlane / pararius.com
49
+ 'div.vc div.text-box h2'
50
+ ]
51
+
52
+ TURNSTILE_SELECTORS = [
53
+ "input[name='cf-turnstile-response']"
54
+ ]
55
+
56
+ SHORT_TIMEOUT = 1
57
+ SESSIONS_STORAGE = SessionsStorage()
58
+
59
+
60
+ def test_browser_installation():
61
+ logging.info("Testing web browser installation...")
62
+ logging.info("Platform: " + platform.platform())
63
+
64
+ chrome_exe_path = utils.get_chrome_exe_path()
65
+ if chrome_exe_path is None:
66
+ logging.error("Chrome / Chromium web browser not installed!")
67
+ sys.exit(1)
68
+ else:
69
+ logging.info("Chrome / Chromium path: " + chrome_exe_path)
70
+
71
+ chrome_major_version = utils.get_chrome_major_version()
72
+ if chrome_major_version == '':
73
+ logging.error("Chrome / Chromium version not detected!")
74
+ sys.exit(1)
75
+ else:
76
+ logging.info("Chrome / Chromium major version: " + chrome_major_version)
77
+
78
+ logging.info("Launching web browser...")
79
+ user_agent = utils.get_user_agent()
80
+ logging.info("FlareSolverr User-Agent: " + user_agent)
81
+ logging.info("Test successful!")
82
+
83
+
84
+ def index_endpoint() -> IndexResponse:
85
+ res = IndexResponse({})
86
+ res.msg = "FlareSolverr is ready!"
87
+ res.version = utils.get_flaresolverr_version()
88
+ res.userAgent = utils.get_user_agent()
89
+ return res
90
+
91
+
92
+ def health_endpoint() -> HealthResponse:
93
+ res = HealthResponse({})
94
+ res.status = STATUS_OK
95
+ return res
96
+
97
+
98
+ def controller_v1_endpoint(req: V1RequestBase) -> V1ResponseBase:
99
+ start_ts = int(time.time() * 1000)
100
+ logging.info(f"Incoming request => POST /v1 body: {utils.object_to_dict(req)}")
101
+ res: V1ResponseBase
102
+ try:
103
+ res = _controller_v1_handler(req)
104
+ except Exception as e:
105
+ res = V1ResponseBase({})
106
+ res.__error_500__ = True
107
+ res.status = STATUS_ERROR
108
+ res.message = "Error: " + str(e)
109
+ logging.error(res.message)
110
+
111
+ res.startTimestamp = start_ts
112
+ res.endTimestamp = int(time.time() * 1000)
113
+ res.version = utils.get_flaresolverr_version()
114
+ logging.debug(f"Response => POST /v1 body: {utils.object_to_dict(res)}")
115
+ logging.info(f"Response in {(res.endTimestamp - res.startTimestamp) / 1000} s")
116
+ return res
117
+
118
+
119
+ def _controller_v1_handler(req: V1RequestBase) -> V1ResponseBase:
120
+ # do some validations
121
+ if req.cmd is None:
122
+ raise Exception("Request parameter 'cmd' is mandatory.")
123
+ if req.headers is not None:
124
+ logging.warning("Request parameter 'headers' was removed in FlareSolverr v2.")
125
+ if req.userAgent is not None:
126
+ logging.warning("Request parameter 'userAgent' was removed in FlareSolverr v2.")
127
+
128
+ # set default values
129
+ if req.maxTimeout is None or int(req.maxTimeout) < 1:
130
+ req.maxTimeout = 60000
131
+
132
+ # execute the command
133
+ res: V1ResponseBase
134
+ if req.cmd == 'sessions.create':
135
+ res = _cmd_sessions_create(req)
136
+ elif req.cmd == 'sessions.list':
137
+ res = _cmd_sessions_list(req)
138
+ elif req.cmd == 'sessions.destroy':
139
+ res = _cmd_sessions_destroy(req)
140
+ elif req.cmd == 'request.get':
141
+ res = _cmd_request_get(req)
142
+ elif req.cmd == 'request.post':
143
+ res = _cmd_request_post(req)
144
+ else:
145
+ raise Exception(f"Request parameter 'cmd' = '{req.cmd}' is invalid.")
146
+
147
+ return res
148
+
149
+
150
+ def _cmd_request_get(req: V1RequestBase) -> V1ResponseBase:
151
+ # do some validations
152
+ if req.url is None:
153
+ raise Exception("Request parameter 'url' is mandatory in 'request.get' command.")
154
+ if req.postData is not None:
155
+ raise Exception("Cannot use 'postBody' when sending a GET request.")
156
+ if req.returnRawHtml is not None:
157
+ logging.warning("Request parameter 'returnRawHtml' was removed in FlareSolverr v2.")
158
+ if req.download is not None:
159
+ logging.warning("Request parameter 'download' was removed in FlareSolverr v2.")
160
+
161
+ challenge_res = _resolve_challenge(req, 'GET')
162
+ res = V1ResponseBase({})
163
+ res.status = challenge_res.status
164
+ res.message = challenge_res.message
165
+ res.solution = challenge_res.result
166
+ return res
167
+
168
+
169
+ def _cmd_request_post(req: V1RequestBase) -> V1ResponseBase:
170
+ # do some validations
171
+ if req.postData is None:
172
+ raise Exception("Request parameter 'postData' is mandatory in 'request.post' command.")
173
+ if req.returnRawHtml is not None:
174
+ logging.warning("Request parameter 'returnRawHtml' was removed in FlareSolverr v2.")
175
+ if req.download is not None:
176
+ logging.warning("Request parameter 'download' was removed in FlareSolverr v2.")
177
+
178
+ challenge_res = _resolve_challenge(req, 'POST')
179
+ res = V1ResponseBase({})
180
+ res.status = challenge_res.status
181
+ res.message = challenge_res.message
182
+ res.solution = challenge_res.result
183
+ return res
184
+
185
+
186
+ def _cmd_sessions_create(req: V1RequestBase) -> V1ResponseBase:
187
+ logging.debug("Creating new session...")
188
+
189
+ session, fresh = SESSIONS_STORAGE.create(session_id=req.session, proxy=req.proxy)
190
+ session_id = session.session_id
191
+
192
+ if not fresh:
193
+ return V1ResponseBase({
194
+ "status": STATUS_OK,
195
+ "message": "Session already exists.",
196
+ "session": session_id
197
+ })
198
+
199
+ return V1ResponseBase({
200
+ "status": STATUS_OK,
201
+ "message": "Session created successfully.",
202
+ "session": session_id
203
+ })
204
+
205
+
206
+ def _cmd_sessions_list(req: V1RequestBase) -> V1ResponseBase:
207
+ session_ids = SESSIONS_STORAGE.session_ids()
208
+
209
+ return V1ResponseBase({
210
+ "status": STATUS_OK,
211
+ "message": "",
212
+ "sessions": session_ids
213
+ })
214
+
215
+
216
+ def _cmd_sessions_destroy(req: V1RequestBase) -> V1ResponseBase:
217
+ session_id = req.session
218
+ existed = SESSIONS_STORAGE.destroy(session_id)
219
+
220
+ if not existed:
221
+ raise Exception("The session doesn't exist.")
222
+
223
+ return V1ResponseBase({
224
+ "status": STATUS_OK,
225
+ "message": "The session has been removed."
226
+ })
227
+
228
+
229
+ def _resolve_challenge(req: V1RequestBase, method: str) -> ChallengeResolutionT:
230
+ timeout = int(req.maxTimeout) / 1000
231
+ driver = None
232
+ try:
233
+ if req.session:
234
+ session_id = req.session
235
+ ttl = timedelta(minutes=req.session_ttl_minutes) if req.session_ttl_minutes else None
236
+ session, fresh = SESSIONS_STORAGE.get(session_id, ttl)
237
+
238
+ if fresh:
239
+ logging.debug(f"new session created to perform the request (session_id={session_id})")
240
+ else:
241
+ logging.debug(f"existing session is used to perform the request (session_id={session_id}, "
242
+ f"lifetime={str(session.lifetime())}, ttl={str(ttl)})")
243
+
244
+ driver = session.driver
245
+ else:
246
+ driver = utils.get_webdriver(req.proxy)
247
+ logging.debug('New instance of webdriver has been created to perform the request')
248
+ return func_timeout(timeout, _evil_logic, (req, driver, method))
249
+ except FunctionTimedOut:
250
+ raise Exception(f'Error solving the challenge. Timeout after {timeout} seconds.')
251
+ except Exception as e:
252
+ raise Exception('Error solving the challenge. ' + str(e).replace('\n', '\\n'))
253
+ finally:
254
+ if not req.session and driver is not None:
255
+ if utils.PLATFORM_VERSION == "nt":
256
+ driver.close()
257
+ driver.quit()
258
+ logging.debug('A used instance of webdriver has been destroyed')
259
+
260
+
261
+ def click_verify(driver: WebDriver, num_tabs: int = 1):
262
+ try:
263
+ logging.debug("Try to find the Cloudflare verify checkbox...")
264
+ actions = ActionChains(driver)
265
+ actions.pause(5)
266
+ for _ in range(num_tabs):
267
+ actions.send_keys(Keys.TAB).pause(0.1)
268
+ actions.pause(1)
269
+ actions.send_keys(Keys.SPACE).perform()
270
+
271
+ logging.debug(f"Cloudflare verify checkbox clicked after {num_tabs} tabs!")
272
+ except Exception:
273
+ logging.debug("Cloudflare verify checkbox not found on the page.")
274
+ finally:
275
+ driver.switch_to.default_content()
276
+
277
+ try:
278
+ logging.debug("Try to find the Cloudflare 'Verify you are human' button...")
279
+ button = driver.find_element(
280
+ by=By.XPATH,
281
+ value="//input[@type='button' and @value='Verify you are human']",
282
+ )
283
+ if button:
284
+ actions = ActionChains(driver)
285
+ actions.move_to_element_with_offset(button, 5, 7)
286
+ actions.click(button)
287
+ actions.perform()
288
+ logging.debug("The Cloudflare 'Verify you are human' button found and clicked!")
289
+ except Exception:
290
+ logging.debug("The Cloudflare 'Verify you are human' button not found on the page.")
291
+
292
+ time.sleep(2)
293
+
294
+ def _get_turnstile_token(driver: WebDriver, tabs: int):
295
+ token_input = driver.find_element(By.CSS_SELECTOR, "input[name='cf-turnstile-response']")
296
+ current_value = token_input.get_attribute("value")
297
+ while True:
298
+ click_verify(driver, num_tabs=tabs)
299
+ turnstile_token = token_input.get_attribute("value")
300
+ if turnstile_token:
301
+ if turnstile_token != current_value:
302
+ logging.info(f"Turnstile token: {turnstile_token}")
303
+ return turnstile_token
304
+ logging.debug(f"Failed to extract token possibly click failed")
305
+
306
+ # reset focus
307
+ driver.execute_script("""
308
+ let el = document.createElement('button');
309
+ el.style.position='fixed';
310
+ el.style.top='0';
311
+ el.style.left='0';
312
+ document.body.prepend(el);
313
+ el.focus();
314
+ """)
315
+ time.sleep(1)
316
+
317
+ def _resolve_turnstile_captcha(req: V1RequestBase, driver: WebDriver):
318
+ turnstile_token = None
319
+ if req.tabs_till_verify is not None:
320
+ logging.debug(f'Navigating to... {req.url} in order to pass the turnstile challenge')
321
+ driver.get(req.url)
322
+
323
+ turnstile_challenge_found = False
324
+ for selector in TURNSTILE_SELECTORS:
325
+ found_elements = driver.find_elements(By.CSS_SELECTOR, selector)
326
+ if len(found_elements) > 0:
327
+ turnstile_challenge_found = True
328
+ logging.info("Turnstile challenge detected. Selector found: " + selector)
329
+ break
330
+ if turnstile_challenge_found:
331
+ turnstile_token = _get_turnstile_token(driver=driver, tabs=req.tabs_till_verify)
332
+ else:
333
+ logging.debug(f'Turnstile challenge not found')
334
+ return turnstile_token
335
+
336
+ def _evil_logic(req: V1RequestBase, driver: WebDriver, method: str) -> ChallengeResolutionT:
337
+ res = ChallengeResolutionT({})
338
+ res.status = STATUS_OK
339
+ res.message = ""
340
+
341
+ # optionally block resources like images/css/fonts using CDP
342
+ disable_media = utils.get_config_disable_media()
343
+ if req.disableMedia is not None:
344
+ disable_media = req.disableMedia
345
+ if disable_media:
346
+ block_urls = [
347
+ # Images
348
+ "*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp", "*.bmp", "*.svg", "*.ico",
349
+ "*.PNG", "*.JPG", "*.JPEG", "*.GIF", "*.WEBP", "*.BMP", "*.SVG", "*.ICO",
350
+ "*.tiff", "*.tif", "*.jpe", "*.apng", "*.avif", "*.heic", "*.heif",
351
+ "*.TIFF", "*.TIF", "*.JPE", "*.APNG", "*.AVIF", "*.HEIC", "*.HEIF",
352
+ # Stylesheets
353
+ "*.css",
354
+ "*.CSS",
355
+ # Fonts
356
+ "*.woff", "*.woff2", "*.ttf", "*.otf", "*.eot",
357
+ "*.WOFF", "*.WOFF2", "*.TTF", "*.OTF", "*.EOT"
358
+ ]
359
+ try:
360
+ logging.debug("Network.setBlockedURLs: %s", block_urls)
361
+ driver.execute_cdp_cmd("Network.enable", {})
362
+ driver.execute_cdp_cmd("Network.setBlockedURLs", {"urls": block_urls})
363
+ except Exception:
364
+ # if CDP commands are not available or fail, ignore and continue
365
+ logging.debug("Network.setBlockedURLs failed or unsupported on this webdriver")
366
+
367
+ # navigate to the page
368
+ logging.debug(f"Navigating to... {req.url}")
369
+ turnstile_token = None
370
+
371
+ if method == "POST":
372
+ _post_request(req, driver)
373
+ else:
374
+ if req.tabs_till_verify is None:
375
+ driver.get(req.url)
376
+ else:
377
+ turnstile_token = _resolve_turnstile_captcha(req, driver)
378
+
379
+ # set cookies if required
380
+ if req.cookies is not None and len(req.cookies) > 0:
381
+ logging.debug(f'Setting cookies...')
382
+ for cookie in req.cookies:
383
+ driver.delete_cookie(cookie['name'])
384
+ driver.add_cookie(cookie)
385
+ # reload the page
386
+ if method == 'POST':
387
+ _post_request(req, driver)
388
+ else:
389
+ driver.get(req.url)
390
+
391
+ # wait for the page
392
+ if utils.get_config_log_html():
393
+ logging.debug(f"Response HTML:\n{driver.page_source}")
394
+ html_element = driver.find_element(By.TAG_NAME, "html")
395
+ page_title = driver.title
396
+
397
+ # find access denied titles
398
+ for title in ACCESS_DENIED_TITLES:
399
+ if page_title.startswith(title):
400
+ raise Exception('Cloudflare has blocked this request. '
401
+ 'Probably your IP is banned for this site, check in your web browser.')
402
+ # find access denied selectors
403
+ for selector in ACCESS_DENIED_SELECTORS:
404
+ found_elements = driver.find_elements(By.CSS_SELECTOR, selector)
405
+ if len(found_elements) > 0:
406
+ raise Exception('Cloudflare has blocked this request. '
407
+ 'Probably your IP is banned for this site, check in your web browser.')
408
+
409
+ # find challenge by title
410
+ challenge_found = False
411
+ for title in CHALLENGE_TITLES:
412
+ if title.lower() == page_title.lower():
413
+ challenge_found = True
414
+ logging.info("Challenge detected. Title found: " + page_title)
415
+ break
416
+ if not challenge_found:
417
+ # find challenge by selectors
418
+ for selector in CHALLENGE_SELECTORS:
419
+ found_elements = driver.find_elements(By.CSS_SELECTOR, selector)
420
+ if len(found_elements) > 0:
421
+ challenge_found = True
422
+ logging.info("Challenge detected. Selector found: " + selector)
423
+ break
424
+
425
+ attempt = 0
426
+ if challenge_found:
427
+ while True:
428
+ try:
429
+ attempt = attempt + 1
430
+ # wait until the title changes
431
+ for title in CHALLENGE_TITLES:
432
+ logging.debug("Waiting for title (attempt " + str(attempt) + "): " + title)
433
+ WebDriverWait(driver, SHORT_TIMEOUT).until_not(title_is(title))
434
+
435
+ # then wait until all the selectors disappear
436
+ for selector in CHALLENGE_SELECTORS:
437
+ logging.debug("Waiting for selector (attempt " + str(attempt) + "): " + selector)
438
+ WebDriverWait(driver, SHORT_TIMEOUT).until_not(
439
+ presence_of_element_located((By.CSS_SELECTOR, selector)))
440
+
441
+ # all elements not found
442
+ break
443
+
444
+ except TimeoutException:
445
+ logging.debug("Timeout waiting for selector")
446
+
447
+ click_verify(driver)
448
+
449
+ # update the html (cloudflare reloads the page every 5 s)
450
+ html_element = driver.find_element(By.TAG_NAME, "html")
451
+
452
+ # waits until cloudflare redirection ends
453
+ logging.debug("Waiting for redirect")
454
+ # noinspection PyBroadException
455
+ try:
456
+ WebDriverWait(driver, SHORT_TIMEOUT).until(staleness_of(html_element))
457
+ except Exception:
458
+ logging.debug("Timeout waiting for redirect")
459
+
460
+ logging.info("Challenge solved!")
461
+ res.message = "Challenge solved!"
462
+ else:
463
+ logging.info("Challenge not detected!")
464
+ res.message = "Challenge not detected!"
465
+
466
+ challenge_res = ChallengeResolutionResultT({})
467
+ challenge_res.url = driver.current_url
468
+ challenge_res.status = 200 # todo: fix, selenium not provides this info
469
+ challenge_res.cookies = driver.get_cookies()
470
+ challenge_res.userAgent = utils.get_user_agent(driver)
471
+ challenge_res.turnstile_token = turnstile_token
472
+
473
+ if not req.returnOnlyCookies:
474
+ challenge_res.headers = {} # todo: fix, selenium not provides this info
475
+
476
+ if req.waitInSeconds and req.waitInSeconds > 0:
477
+ logging.info("Waiting " + str(req.waitInSeconds) + " seconds before returning the response...")
478
+ time.sleep(req.waitInSeconds)
479
+
480
+ challenge_res.response = driver.page_source
481
+
482
+ if req.returnScreenshot:
483
+ challenge_res.screenshot = driver.get_screenshot_as_base64()
484
+
485
+ res.result = challenge_res
486
+ return res
487
+
488
+
489
+ def _post_request(req: V1RequestBase, driver: WebDriver):
490
+ post_form = f'<form id="hackForm" action="{req.url}" method="POST">'
491
+ query_string = req.postData if req.postData and req.postData[0] != '?' else req.postData[1:] if req.postData else ''
492
+ pairs = query_string.split('&')
493
+ for pair in pairs:
494
+ parts = pair.split('=', 1)
495
+ # noinspection PyBroadException
496
+ try:
497
+ name = unquote(parts[0])
498
+ except Exception:
499
+ name = parts[0]
500
+ if name == 'submit':
501
+ continue
502
+ # noinspection PyBroadException
503
+ try:
504
+ value = unquote(parts[1]) if len(parts) > 1 else ''
505
+ except Exception:
506
+ value = parts[1] if len(parts) > 1 else ''
507
+ # Protection of " character, for syntax
508
+ value=value.replace('"','&quot;')
509
+ post_form += f'<input type="text" name="{escape(quote(name))}" value="{escape(quote(value))}"><br>'
510
+ post_form += '</form>'
511
+ html_content = f"""
512
+ <!DOCTYPE html>
513
+ <html>
514
+ <body>
515
+ {post_form}
516
+ <script>document.getElementById('hackForm').submit();</script>
517
+ </body>
518
+ </html>"""
519
+ driver.get("data:text/html;charset=utf-8,{html_content}".format(html_content=html_content))
src/metrics.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ from prometheus_client import Counter, Histogram, start_http_server
4
+ import time
5
+
6
+ REQUEST_COUNTER = Counter(
7
+ name='flaresolverr_request',
8
+ documentation='Total requests with result',
9
+ labelnames=['domain', 'result']
10
+ )
11
+ REQUEST_DURATION = Histogram(
12
+ name='flaresolverr_request_duration',
13
+ documentation='Request duration in seconds',
14
+ labelnames=['domain'],
15
+ buckets=[0, 10, 25, 50]
16
+ )
17
+
18
+
19
+ def serve(port):
20
+ start_http_server(port=port)
21
+ while True:
22
+ time.sleep(600)
23
+
24
+
25
+ def start_metrics_http_server(prometheus_port: int):
26
+ logging.info(f"Serving Prometheus exporter on http://0.0.0.0:{prometheus_port}/metrics")
27
+ from threading import Thread
28
+ Thread(
29
+ target=serve,
30
+ kwargs=dict(port=prometheus_port),
31
+ daemon=True,
32
+ ).start()
src/sessions.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from dataclasses import dataclass
3
+ from datetime import datetime, timedelta
4
+ from typing import Optional, Tuple
5
+ from uuid import uuid1
6
+
7
+ from selenium.webdriver.chrome.webdriver import WebDriver
8
+
9
+ import utils
10
+
11
+
12
+ @dataclass
13
+ class Session:
14
+ session_id: str
15
+ driver: WebDriver
16
+ created_at: datetime
17
+
18
+ def lifetime(self) -> timedelta:
19
+ return datetime.now() - self.created_at
20
+
21
+
22
+ class SessionsStorage:
23
+ """SessionsStorage creates, stores and process all the sessions"""
24
+
25
+ def __init__(self):
26
+ self.sessions = {}
27
+
28
+ def create(self, session_id: Optional[str] = None, proxy: Optional[dict] = None,
29
+ force_new: Optional[bool] = False) -> Tuple[Session, bool]:
30
+ """create creates new instance of WebDriver if necessary,
31
+ assign defined (or newly generated) session_id to the instance
32
+ and returns the session object. If a new session has been created
33
+ second argument is set to True.
34
+
35
+ Note: The function is idempotent, so in case if session_id
36
+ already exists in the storage a new instance of WebDriver won't be created
37
+ and existing session will be returned. Second argument defines if
38
+ new session has been created (True) or an existing one was used (False).
39
+ """
40
+ session_id = session_id or str(uuid1())
41
+
42
+ if force_new:
43
+ self.destroy(session_id)
44
+
45
+ if self.exists(session_id):
46
+ return self.sessions[session_id], False
47
+
48
+ driver = utils.get_webdriver(proxy)
49
+ created_at = datetime.now()
50
+ session = Session(session_id, driver, created_at)
51
+
52
+ self.sessions[session_id] = session
53
+
54
+ return session, True
55
+
56
+ def exists(self, session_id: str) -> bool:
57
+ return session_id in self.sessions
58
+
59
+ def destroy(self, session_id: str) -> bool:
60
+ """destroy closes the driver instance and removes session from the storage.
61
+ The function is noop if session_id doesn't exist.
62
+ The function returns True if session was found and destroyed,
63
+ and False if session_id wasn't found.
64
+ """
65
+ if not self.exists(session_id):
66
+ return False
67
+
68
+ session = self.sessions.pop(session_id)
69
+ if utils.PLATFORM_VERSION == "nt":
70
+ session.driver.close()
71
+ session.driver.quit()
72
+ return True
73
+
74
+ def get(self, session_id: str, ttl: Optional[timedelta] = None) -> Tuple[Session, bool]:
75
+ session, fresh = self.create(session_id)
76
+
77
+ if ttl is not None and not fresh and session.lifetime() > ttl:
78
+ logging.debug(f'session\'s lifetime has expired, so the session is recreated (session_id={session_id})')
79
+ session, fresh = self.create(session_id, force_new=True)
80
+
81
+ return session, fresh
82
+
83
+ def session_ids(self) -> list[str]:
84
+ return list(self.sessions.keys())
src/tests.py ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ from typing import Optional
3
+
4
+ from webtest import TestApp
5
+
6
+ from dtos import IndexResponse, HealthResponse, V1ResponseBase, STATUS_OK, STATUS_ERROR
7
+ import flaresolverr
8
+ import utils
9
+
10
+
11
+ def _find_obj_by_key(key: str, value: str, _list: list) -> Optional[dict]:
12
+ for obj in _list:
13
+ if obj[key] == value:
14
+ return obj
15
+ return None
16
+
17
+
18
+ class TestFlareSolverr(unittest.TestCase):
19
+
20
+ proxy_url = "http://127.0.0.1:8888"
21
+ proxy_socks_url = "socks5://127.0.0.1:1080"
22
+ google_url = "https://www.google.com"
23
+ post_url = "https://httpbin.org/post"
24
+ cloudflare_url = "https://nowsecure.nl/"
25
+ cloudflare_url_2 = "https://idope.se/torrent-list/harry/"
26
+ ddos_guard_url = "https://www.litres.ru/"
27
+ fairlane_url = "https://www.pararius.com/apartments/amsterdam"
28
+ custom_cloudflare_url = "https://www.muziekfabriek.org/"
29
+ cloudflare_blocked_url = "https://cpasbiens3.fr/index.php?do=search&subaction=search"
30
+
31
+ app = TestApp(flaresolverr.app)
32
+ # wait until the server is ready
33
+ app.get('/')
34
+
35
+ def test_wrong_endpoint(self):
36
+ res = self.app.get('/wrong', status=404)
37
+ self.assertEqual(res.status_code, 404)
38
+
39
+ body = res.json
40
+ self.assertEqual("Not found: '/wrong'", body['error'])
41
+ self.assertEqual(404, body['status_code'])
42
+
43
+ def test_index_endpoint(self):
44
+ res = self.app.get('/')
45
+ self.assertEqual(res.status_code, 200)
46
+
47
+ body = IndexResponse(res.json)
48
+ self.assertEqual("FlareSolverr is ready!", body.msg)
49
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
50
+ self.assertIn("Chrome/", body.userAgent)
51
+
52
+ def test_health_endpoint(self):
53
+ res = self.app.get('/health')
54
+ self.assertEqual(res.status_code, 200)
55
+
56
+ body = HealthResponse(res.json)
57
+ self.assertEqual(STATUS_OK, body.status)
58
+
59
+ def test_v1_endpoint_wrong_cmd(self):
60
+ res = self.app.post_json('/v1', {
61
+ "cmd": "request.bad",
62
+ "url": self.google_url
63
+ }, status=500)
64
+ self.assertEqual(res.status_code, 500)
65
+
66
+ body = V1ResponseBase(res.json)
67
+ self.assertEqual(STATUS_ERROR, body.status)
68
+ self.assertEqual("Error: Request parameter 'cmd' = 'request.bad' is invalid.", body.message)
69
+ self.assertGreater(body.startTimestamp, 10000)
70
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
71
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
72
+
73
+ def test_v1_endpoint_request_get_no_cloudflare(self):
74
+ res = self.app.post_json('/v1', {
75
+ "cmd": "request.get",
76
+ "url": self.google_url
77
+ })
78
+ self.assertEqual(res.status_code, 200)
79
+
80
+ body = V1ResponseBase(res.json)
81
+ self.assertEqual(STATUS_OK, body.status)
82
+ self.assertEqual("Challenge not detected!", body.message)
83
+ self.assertGreater(body.startTimestamp, 10000)
84
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
85
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
86
+
87
+ solution = body.solution
88
+ self.assertIn(self.google_url, solution.url)
89
+ self.assertEqual(solution.status, 200)
90
+ self.assertIs(len(solution.headers), 0)
91
+ self.assertIn("<title>Google</title>", solution.response)
92
+ self.assertGreater(len(solution.cookies), 0)
93
+ self.assertIn("Chrome/", solution.userAgent)
94
+
95
+ def test_v1_endpoint_request_get_disable_resources(self):
96
+ res = self.app.post_json("/v1", {
97
+ "cmd": "request.get",
98
+ "url": self.google_url,
99
+ "disableMedia": True
100
+ })
101
+ self.assertEqual(res.status_code, 200)
102
+
103
+ body = V1ResponseBase(res.json)
104
+ self.assertEqual(STATUS_OK, body.status)
105
+ self.assertEqual("Challenge not detected!", body.message)
106
+ self.assertGreater(body.startTimestamp, 10000)
107
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
108
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
109
+
110
+ solution = body.solution
111
+ self.assertIn(self.google_url, solution.url)
112
+ self.assertEqual(solution.status, 200)
113
+ self.assertIs(len(solution.headers), 0)
114
+ self.assertIn("<title>Google</title>", solution.response)
115
+ self.assertGreater(len(solution.cookies), 0)
116
+ self.assertIn("Chrome/", solution.userAgent)
117
+
118
+ def test_v1_endpoint_request_get_cloudflare_js_1(self):
119
+ res = self.app.post_json('/v1', {
120
+ "cmd": "request.get",
121
+ "url": self.cloudflare_url
122
+ })
123
+ self.assertEqual(res.status_code, 200)
124
+
125
+ body = V1ResponseBase(res.json)
126
+ self.assertEqual(STATUS_OK, body.status)
127
+ self.assertEqual("Challenge solved!", body.message)
128
+ self.assertGreater(body.startTimestamp, 10000)
129
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
130
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
131
+
132
+ solution = body.solution
133
+ self.assertIn(self.cloudflare_url, solution.url)
134
+ self.assertEqual(solution.status, 200)
135
+ self.assertIs(len(solution.headers), 0)
136
+ self.assertIn("<title>nowSecure</title>", solution.response)
137
+ self.assertGreater(len(solution.cookies), 0)
138
+ self.assertIn("Chrome/", solution.userAgent)
139
+
140
+ cf_cookie = _find_obj_by_key("name", "cf_clearance", solution.cookies)
141
+ self.assertIsNotNone(cf_cookie, "Cloudflare cookie not found")
142
+ self.assertGreater(len(cf_cookie["value"]), 30)
143
+
144
+ def test_v1_endpoint_request_get_cloudflare_js_2(self):
145
+ res = self.app.post_json('/v1', {
146
+ "cmd": "request.get",
147
+ "url": self.cloudflare_url_2
148
+ })
149
+ self.assertEqual(res.status_code, 200)
150
+
151
+ body = V1ResponseBase(res.json)
152
+ self.assertEqual(STATUS_OK, body.status)
153
+ self.assertEqual("Challenge solved!", body.message)
154
+ self.assertGreater(body.startTimestamp, 10000)
155
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
156
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
157
+
158
+ solution = body.solution
159
+ self.assertIn(self.cloudflare_url_2, solution.url)
160
+ self.assertEqual(solution.status, 200)
161
+ self.assertIs(len(solution.headers), 0)
162
+ self.assertIn("<title>harry - idope torrent search</title>", solution.response)
163
+ self.assertGreater(len(solution.cookies), 0)
164
+ self.assertIn("Chrome/", solution.userAgent)
165
+
166
+ cf_cookie = _find_obj_by_key("name", "cf_clearance", solution.cookies)
167
+ self.assertIsNotNone(cf_cookie, "Cloudflare cookie not found")
168
+ self.assertGreater(len(cf_cookie["value"]), 30)
169
+
170
+ def test_v1_endpoint_request_get_ddos_guard_js(self):
171
+ res = self.app.post_json('/v1', {
172
+ "cmd": "request.get",
173
+ "url": self.ddos_guard_url
174
+ })
175
+ self.assertEqual(res.status_code, 200)
176
+
177
+ body = V1ResponseBase(res.json)
178
+ self.assertEqual(STATUS_OK, body.status)
179
+ self.assertEqual("Challenge solved!", body.message)
180
+ self.assertGreater(body.startTimestamp, 10000)
181
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
182
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
183
+
184
+ solution = body.solution
185
+ self.assertIn(self.ddos_guard_url, solution.url)
186
+ self.assertEqual(solution.status, 200)
187
+ self.assertIs(len(solution.headers), 0)
188
+ self.assertIn("<title>Литрес", solution.response)
189
+ self.assertGreater(len(solution.cookies), 0)
190
+ self.assertIn("Chrome/", solution.userAgent)
191
+
192
+ cf_cookie = _find_obj_by_key("name", "__ddg1_", solution.cookies)
193
+ self.assertIsNotNone(cf_cookie, "DDOS-Guard cookie not found")
194
+ self.assertGreater(len(cf_cookie["value"]), 10)
195
+
196
+ def test_v1_endpoint_request_get_fairlane_js(self):
197
+ res = self.app.post_json('/v1', {
198
+ "cmd": "request.get",
199
+ "url": self.fairlane_url
200
+ })
201
+ self.assertEqual(res.status_code, 200)
202
+
203
+ body = V1ResponseBase(res.json)
204
+ self.assertEqual(STATUS_OK, body.status)
205
+ self.assertEqual("Challenge solved!", body.message)
206
+ self.assertGreater(body.startTimestamp, 10000)
207
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
208
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
209
+
210
+ solution = body.solution
211
+ self.assertIn(self.fairlane_url, solution.url)
212
+ self.assertEqual(solution.status, 200)
213
+ self.assertIs(len(solution.headers), 0)
214
+ self.assertIn("<title>Rental Apartments Amsterdam</title>", solution.response)
215
+ self.assertGreater(len(solution.cookies), 0)
216
+ self.assertIn("Chrome/", solution.userAgent)
217
+
218
+ cf_cookie = _find_obj_by_key("name", "fl_pass_v2_b", solution.cookies)
219
+ self.assertIsNotNone(cf_cookie, "Fairlane cookie not found")
220
+ self.assertGreater(len(cf_cookie["value"]), 50)
221
+
222
+ def test_v1_endpoint_request_get_custom_cloudflare_js(self):
223
+ res = self.app.post_json('/v1', {
224
+ "cmd": "request.get",
225
+ "url": self.custom_cloudflare_url
226
+ })
227
+ self.assertEqual(res.status_code, 200)
228
+
229
+ body = V1ResponseBase(res.json)
230
+ self.assertEqual(STATUS_OK, body.status)
231
+ self.assertEqual("Challenge solved!", body.message)
232
+ self.assertGreater(body.startTimestamp, 10000)
233
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
234
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
235
+
236
+ solution = body.solution
237
+ self.assertIn(self.custom_cloudflare_url, solution.url)
238
+ self.assertEqual(solution.status, 200)
239
+ self.assertIs(len(solution.headers), 0)
240
+ self.assertIn("<title>MuziekFabriek : Aanmelden</title>", solution.response)
241
+ self.assertGreater(len(solution.cookies), 0)
242
+ self.assertIn("Chrome/", solution.userAgent)
243
+
244
+ cf_cookie = _find_obj_by_key("name", "ct_anti_ddos_key", solution.cookies)
245
+ self.assertIsNotNone(cf_cookie, "Custom Cloudflare cookie not found")
246
+ self.assertGreater(len(cf_cookie["value"]), 10)
247
+
248
+ # todo: test Cmd 'request.get' should return fail with Cloudflare CAPTCHA
249
+
250
+ def test_v1_endpoint_request_get_cloudflare_blocked(self):
251
+ res = self.app.post_json('/v1', {
252
+ "cmd": "request.get",
253
+ "url": self.cloudflare_blocked_url
254
+ }, status=500)
255
+ self.assertEqual(res.status_code, 500)
256
+
257
+ body = V1ResponseBase(res.json)
258
+ self.assertEqual(STATUS_ERROR, body.status)
259
+ self.assertEqual("Error: Error solving the challenge. Cloudflare has blocked this request. "
260
+ "Probably your IP is banned for this site, check in your web browser.", body.message)
261
+ self.assertGreater(body.startTimestamp, 10000)
262
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
263
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
264
+
265
+ def test_v1_endpoint_request_get_cookies_param(self):
266
+ res = self.app.post_json('/v1', {
267
+ "cmd": "request.get",
268
+ "url": self.google_url,
269
+ "cookies": [
270
+ {
271
+ "name": "testcookie1",
272
+ "value": "testvalue1"
273
+ },
274
+ {
275
+ "name": "testcookie2",
276
+ "value": "testvalue2"
277
+ }
278
+ ]
279
+ })
280
+ self.assertEqual(res.status_code, 200)
281
+
282
+ body = V1ResponseBase(res.json)
283
+ self.assertEqual(STATUS_OK, body.status)
284
+ self.assertEqual("Challenge not detected!", body.message)
285
+ self.assertGreater(body.startTimestamp, 10000)
286
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
287
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
288
+
289
+ solution = body.solution
290
+ self.assertIn(self.google_url, solution.url)
291
+ self.assertEqual(solution.status, 200)
292
+ self.assertIs(len(solution.headers), 0)
293
+ self.assertIn("<title>Google</title>", solution.response)
294
+ self.assertGreater(len(solution.cookies), 1)
295
+ self.assertIn("Chrome/", solution.userAgent)
296
+
297
+ user_cookie1 = _find_obj_by_key("name", "testcookie1", solution.cookies)
298
+ self.assertIsNotNone(user_cookie1, "User cookie 1 not found")
299
+ self.assertEqual("testvalue1", user_cookie1["value"])
300
+
301
+ user_cookie2 = _find_obj_by_key("name", "testcookie2", solution.cookies)
302
+ self.assertIsNotNone(user_cookie2, "User cookie 2 not found")
303
+ self.assertEqual("testvalue2", user_cookie2["value"])
304
+
305
+ def test_v1_endpoint_request_get_returnOnlyCookies_param(self):
306
+ res = self.app.post_json('/v1', {
307
+ "cmd": "request.get",
308
+ "url": self.google_url,
309
+ "returnOnlyCookies": True
310
+ })
311
+ self.assertEqual(res.status_code, 200)
312
+
313
+ body = V1ResponseBase(res.json)
314
+ self.assertEqual(STATUS_OK, body.status)
315
+ self.assertEqual("Challenge not detected!", body.message)
316
+ self.assertGreater(body.startTimestamp, 10000)
317
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
318
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
319
+
320
+ solution = body.solution
321
+ self.assertIn(self.google_url, solution.url)
322
+ self.assertEqual(solution.status, 200)
323
+ self.assertIsNone(solution.headers)
324
+ self.assertIsNone(solution.response)
325
+ self.assertGreater(len(solution.cookies), 0)
326
+ self.assertIn("Chrome/", solution.userAgent)
327
+
328
+ def test_v1_endpoint_request_get_proxy_http_param(self):
329
+ """
330
+ To configure TinyProxy in local:
331
+ * sudo vim /etc/tinyproxy/tinyproxy.conf
332
+ * edit => LogFile "/tmp/tinyproxy.log"
333
+ * edit => Syslog Off
334
+ * sudo tinyproxy -d
335
+ * sudo tail -f /tmp/tinyproxy.log
336
+ """
337
+ res = self.app.post_json('/v1', {
338
+ "cmd": "request.get",
339
+ "url": self.google_url,
340
+ "proxy": {
341
+ "url": self.proxy_url
342
+ }
343
+ })
344
+ self.assertEqual(res.status_code, 200)
345
+
346
+ body = V1ResponseBase(res.json)
347
+ self.assertEqual(STATUS_OK, body.status)
348
+ self.assertEqual("Challenge not detected!", body.message)
349
+ self.assertGreater(body.startTimestamp, 10000)
350
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
351
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
352
+
353
+ solution = body.solution
354
+ self.assertIn(self.google_url, solution.url)
355
+ self.assertEqual(solution.status, 200)
356
+ self.assertIs(len(solution.headers), 0)
357
+ self.assertIn("<title>Google</title>", solution.response)
358
+ self.assertGreater(len(solution.cookies), 0)
359
+ self.assertIn("Chrome/", solution.userAgent)
360
+
361
+ def test_v1_endpoint_request_get_proxy_http_param_with_credentials(self):
362
+ """
363
+ To configure TinyProxy in local:
364
+ * sudo vim /etc/tinyproxy/tinyproxy.conf
365
+ * edit => LogFile "/tmp/tinyproxy.log"
366
+ * edit => Syslog Off
367
+ * add => BasicAuth testuser testpass
368
+ * sudo tinyproxy -d
369
+ * sudo tail -f /tmp/tinyproxy.log
370
+ """
371
+ res = self.app.post_json('/v1', {
372
+ "cmd": "request.get",
373
+ "url": self.google_url,
374
+ "proxy": {
375
+ "url": self.proxy_url,
376
+ "username": "testuser",
377
+ "password": "testpass"
378
+ }
379
+ })
380
+ self.assertEqual(res.status_code, 200)
381
+
382
+ body = V1ResponseBase(res.json)
383
+ self.assertEqual(STATUS_OK, body.status)
384
+ self.assertEqual("Challenge not detected!", body.message)
385
+ self.assertGreater(body.startTimestamp, 10000)
386
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
387
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
388
+
389
+ solution = body.solution
390
+ self.assertIn(self.google_url, solution.url)
391
+ self.assertEqual(solution.status, 200)
392
+ self.assertIs(len(solution.headers), 0)
393
+ self.assertIn("<title>Google</title>", solution.response)
394
+ self.assertGreater(len(solution.cookies), 0)
395
+ self.assertIn("Chrome/", solution.userAgent)
396
+
397
+ def test_v1_endpoint_request_get_proxy_socks_param(self):
398
+ """
399
+ To configure Dante in local:
400
+ * https://linuxhint.com/set-up-a-socks5-proxy-on-ubuntu-with-dante/
401
+ * sudo vim /etc/sockd.conf
402
+ * sudo systemctl restart sockd.service
403
+ * curl --socks5 socks5://127.0.0.1:1080 https://www.google.com
404
+ """
405
+ res = self.app.post_json('/v1', {
406
+ "cmd": "request.get",
407
+ "url": self.google_url,
408
+ "proxy": {
409
+ "url": self.proxy_socks_url
410
+ }
411
+ })
412
+ self.assertEqual(res.status_code, 200)
413
+
414
+ body = V1ResponseBase(res.json)
415
+ self.assertEqual(STATUS_OK, body.status)
416
+ self.assertEqual("Challenge not detected!", body.message)
417
+ self.assertGreater(body.startTimestamp, 10000)
418
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
419
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
420
+
421
+ solution = body.solution
422
+ self.assertIn(self.google_url, solution.url)
423
+ self.assertEqual(solution.status, 200)
424
+ self.assertIs(len(solution.headers), 0)
425
+ self.assertIn("<title>Google</title>", solution.response)
426
+ self.assertGreater(len(solution.cookies), 0)
427
+ self.assertIn("Chrome/", solution.userAgent)
428
+
429
+ def test_v1_endpoint_request_get_proxy_wrong_param(self):
430
+ res = self.app.post_json('/v1', {
431
+ "cmd": "request.get",
432
+ "url": self.google_url,
433
+ "proxy": {
434
+ "url": "http://127.0.0.1:43210"
435
+ }
436
+ }, status=500)
437
+ self.assertEqual(res.status_code, 500)
438
+
439
+ body = V1ResponseBase(res.json)
440
+ self.assertEqual(STATUS_ERROR, body.status)
441
+ self.assertIn("Error: Error solving the challenge. Message: unknown error: net::ERR_PROXY_CONNECTION_FAILED",
442
+ body.message)
443
+ self.assertGreater(body.startTimestamp, 10000)
444
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
445
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
446
+
447
+ def test_v1_endpoint_request_get_fail_timeout(self):
448
+ res = self.app.post_json('/v1', {
449
+ "cmd": "request.get",
450
+ "url": self.google_url,
451
+ "maxTimeout": 10
452
+ }, status=500)
453
+ self.assertEqual(res.status_code, 500)
454
+
455
+ body = V1ResponseBase(res.json)
456
+ self.assertEqual(STATUS_ERROR, body.status)
457
+ self.assertEqual("Error: Error solving the challenge. Timeout after 0.01 seconds.", body.message)
458
+ self.assertGreater(body.startTimestamp, 10000)
459
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
460
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
461
+
462
+ def test_v1_endpoint_request_get_fail_bad_domain(self):
463
+ res = self.app.post_json('/v1', {
464
+ "cmd": "request.get",
465
+ "url": "https://www.google.combad"
466
+ }, status=500)
467
+ self.assertEqual(res.status_code, 500)
468
+
469
+ body = V1ResponseBase(res.json)
470
+ self.assertEqual(STATUS_ERROR, body.status)
471
+ self.assertIn("Message: unknown error: net::ERR_NAME_NOT_RESOLVED", body.message)
472
+
473
+ def test_v1_endpoint_request_get_deprecated_param(self):
474
+ res = self.app.post_json('/v1', {
475
+ "cmd": "request.get",
476
+ "url": self.google_url,
477
+ "userAgent": "Test User-Agent" # was removed in v2, not used
478
+ })
479
+ self.assertEqual(res.status_code, 200)
480
+
481
+ body = V1ResponseBase(res.json)
482
+ self.assertEqual(STATUS_OK, body.status)
483
+ self.assertEqual("Challenge not detected!", body.message)
484
+
485
+ def test_v1_endpoint_request_post_no_cloudflare(self):
486
+ res = self.app.post_json('/v1', {
487
+ "cmd": "request.post",
488
+ "url": self.post_url,
489
+ "postData": "param1=value1&param2=value2"
490
+ })
491
+ self.assertEqual(res.status_code, 200)
492
+
493
+ body = V1ResponseBase(res.json)
494
+ self.assertEqual(STATUS_OK, body.status)
495
+ self.assertEqual("Challenge not detected!", body.message)
496
+ self.assertGreater(body.startTimestamp, 10000)
497
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
498
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
499
+
500
+ solution = body.solution
501
+ self.assertIn(self.post_url, solution.url)
502
+ self.assertEqual(solution.status, 200)
503
+ self.assertIs(len(solution.headers), 0)
504
+ self.assertIn('"form": {\n "param1": "value1", \n "param2": "value2"\n }', solution.response)
505
+ self.assertEqual(len(solution.cookies), 0)
506
+ self.assertIn("Chrome/", solution.userAgent)
507
+
508
+ def test_v1_endpoint_request_post_cloudflare(self):
509
+ res = self.app.post_json('/v1', {
510
+ "cmd": "request.post",
511
+ "url": self.cloudflare_url,
512
+ "postData": "param1=value1&param2=value2"
513
+ })
514
+ self.assertEqual(res.status_code, 200)
515
+
516
+ body = V1ResponseBase(res.json)
517
+ self.assertEqual(STATUS_OK, body.status)
518
+ self.assertEqual("Challenge solved!", body.message)
519
+ self.assertGreater(body.startTimestamp, 10000)
520
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
521
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
522
+
523
+ solution = body.solution
524
+ self.assertIn(self.cloudflare_url, solution.url)
525
+ self.assertEqual(solution.status, 200)
526
+ self.assertIs(len(solution.headers), 0)
527
+ self.assertIn("<title>405 Not Allowed</title>", solution.response)
528
+ self.assertGreater(len(solution.cookies), 0)
529
+ self.assertIn("Chrome/", solution.userAgent)
530
+
531
+ cf_cookie = _find_obj_by_key("name", "cf_clearance", solution.cookies)
532
+ self.assertIsNotNone(cf_cookie, "Cloudflare cookie not found")
533
+ self.assertGreater(len(cf_cookie["value"]), 30)
534
+
535
+ def test_v1_endpoint_request_post_fail_no_post_data(self):
536
+ res = self.app.post_json('/v1', {
537
+ "cmd": "request.post",
538
+ "url": self.google_url
539
+ }, status=500)
540
+ self.assertEqual(res.status_code, 500)
541
+
542
+ body = V1ResponseBase(res.json)
543
+ self.assertEqual(STATUS_ERROR, body.status)
544
+ self.assertIn("Request parameter 'postData' is mandatory in 'request.post' command", body.message)
545
+
546
+ def test_v1_endpoint_request_post_deprecated_param(self):
547
+ res = self.app.post_json('/v1', {
548
+ "cmd": "request.post",
549
+ "url": self.google_url,
550
+ "postData": "param1=value1&param2=value2",
551
+ "userAgent": "Test User-Agent" # was removed in v2, not used
552
+ })
553
+ self.assertEqual(res.status_code, 200)
554
+
555
+ body = V1ResponseBase(res.json)
556
+ self.assertEqual(STATUS_OK, body.status)
557
+ self.assertEqual("Challenge not detected!", body.message)
558
+
559
+ def test_v1_endpoint_sessions_create_without_session(self):
560
+ res = self.app.post_json('/v1', {
561
+ "cmd": "sessions.create"
562
+ })
563
+ self.assertEqual(res.status_code, 200)
564
+
565
+ body = V1ResponseBase(res.json)
566
+ self.assertEqual(STATUS_OK, body.status)
567
+ self.assertEqual("Session created successfully.", body.message)
568
+ self.assertIsNotNone(body.session)
569
+
570
+ def test_v1_endpoint_sessions_create_with_session(self):
571
+ res = self.app.post_json('/v1', {
572
+ "cmd": "sessions.create",
573
+ "session": "test_create_session"
574
+ })
575
+ self.assertEqual(res.status_code, 200)
576
+
577
+ body = V1ResponseBase(res.json)
578
+ self.assertEqual(STATUS_OK, body.status)
579
+ self.assertEqual("Session created successfully.", body.message)
580
+ self.assertEqual(body.session, "test_create_session")
581
+
582
+ def test_v1_endpoint_sessions_create_with_proxy(self):
583
+ res = self.app.post_json('/v1', {
584
+ "cmd": "sessions.create",
585
+ "proxy": {
586
+ "url": self.proxy_url
587
+ }
588
+ })
589
+ self.assertEqual(res.status_code, 200)
590
+
591
+ body = V1ResponseBase(res.json)
592
+ self.assertEqual(STATUS_OK, body.status)
593
+ self.assertEqual("Session created successfully.", body.message)
594
+ self.assertIsNotNone(body.session)
595
+
596
+ def test_v1_endpoint_sessions_list(self):
597
+ self.app.post_json('/v1', {
598
+ "cmd": "sessions.create",
599
+ "session": "test_list_sessions"
600
+ })
601
+ res = self.app.post_json('/v1', {
602
+ "cmd": "sessions.list"
603
+ })
604
+ self.assertEqual(res.status_code, 200)
605
+
606
+ body = V1ResponseBase(res.json)
607
+ self.assertEqual(STATUS_OK, body.status)
608
+ self.assertEqual("", body.message)
609
+ self.assertGreaterEqual(len(body.sessions), 1)
610
+ self.assertIn("test_list_sessions", body.sessions)
611
+
612
+ def test_v1_endpoint_sessions_destroy_existing_session(self):
613
+ self.app.post_json('/v1', {
614
+ "cmd": "sessions.create",
615
+ "session": "test_destroy_sessions"
616
+ })
617
+ res = self.app.post_json('/v1', {
618
+ "cmd": "sessions.destroy",
619
+ "session": "test_destroy_sessions"
620
+ })
621
+ self.assertEqual(res.status_code, 200)
622
+
623
+ body = V1ResponseBase(res.json)
624
+ self.assertEqual(STATUS_OK, body.status)
625
+ self.assertEqual("The session has been removed.", body.message)
626
+
627
+ def test_v1_endpoint_sessions_destroy_non_existing_session(self):
628
+ res = self.app.post_json('/v1', {
629
+ "cmd": "sessions.destroy",
630
+ "session": "non_existing_session_name"
631
+ }, status=500)
632
+ self.assertEqual(res.status_code, 500)
633
+
634
+ body = V1ResponseBase(res.json)
635
+ self.assertEqual(STATUS_ERROR, body.status)
636
+ self.assertEqual("Error: The session doesn't exist.", body.message)
637
+
638
+ def test_v1_endpoint_request_get_with_session(self):
639
+ self.app.post_json('/v1', {
640
+ "cmd": "sessions.create",
641
+ "session": "test_request_sessions"
642
+ })
643
+ res = self.app.post_json('/v1', {
644
+ "cmd": "request.get",
645
+ "session": "test_request_sessions",
646
+ "url": self.google_url
647
+ })
648
+ self.assertEqual(res.status_code, 200)
649
+
650
+ body = V1ResponseBase(res.json)
651
+ self.assertEqual(STATUS_OK, body.status)
652
+
653
+
654
+ if __name__ == '__main__':
655
+ unittest.main()
src/tests_sites.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+
3
+ from webtest import TestApp
4
+
5
+ from dtos import V1ResponseBase, STATUS_OK
6
+ import flaresolverr
7
+ import utils
8
+
9
+
10
+ def _find_obj_by_key(key: str, value: str, _list: list) -> dict | None:
11
+ for obj in _list:
12
+ if obj[key] == value:
13
+ return obj
14
+ return None
15
+
16
+
17
+ def asset_cloudflare_solution(self, res, site_url, site_text):
18
+ self.assertEqual(res.status_code, 200)
19
+
20
+ body = V1ResponseBase(res.json)
21
+ self.assertEqual(STATUS_OK, body.status)
22
+ self.assertEqual("Challenge solved!", body.message)
23
+ self.assertGreater(body.startTimestamp, 10000)
24
+ self.assertGreaterEqual(body.endTimestamp, body.startTimestamp)
25
+ self.assertEqual(utils.get_flaresolverr_version(), body.version)
26
+
27
+ solution = body.solution
28
+ self.assertIn(site_url, solution.url)
29
+ self.assertEqual(solution.status, 200)
30
+ self.assertIs(len(solution.headers), 0)
31
+ self.assertIn(site_text, solution.response)
32
+ self.assertGreater(len(solution.cookies), 0)
33
+ self.assertIn("Chrome/", solution.userAgent)
34
+
35
+ cf_cookie = _find_obj_by_key("name", "cf_clearance", solution.cookies)
36
+ self.assertIsNotNone(cf_cookie, "Cloudflare cookie not found")
37
+ self.assertGreater(len(cf_cookie["value"]), 30)
38
+
39
+
40
+ class TestFlareSolverr(unittest.TestCase):
41
+ app = TestApp(flaresolverr.app)
42
+ # wait until the server is ready
43
+ app.get('/')
44
+
45
+ def test_v1_endpoint_request_get_cloudflare(self):
46
+ sites_get = [
47
+ ('nowsecure', 'https://nowsecure.nl', '<title>nowSecure</title>'),
48
+ ('0magnet', 'https://0magnet.com/search?q=2022', 'Torrent Search - ØMagnet'),
49
+ ('1337x', 'https://1337x.unblockit.cat/cat/Movies/time/desc/1/', ''),
50
+ ('avistaz', 'https://avistaz.to/api/v1/jackett/torrents?in=1&type=0&search=',
51
+ '<title>Access denied</title>'),
52
+ ('badasstorrents', 'https://badasstorrents.com/torrents/search/720p/date/desc',
53
+ '<title>Latest Torrents - BadassTorrents</title>'),
54
+ ('bt4g', 'https://bt4g.org/search/2022', '<title>Download 2022 Torrents - BT4G</title>'),
55
+ ('cinemaz', 'https://cinemaz.to/api/v1/jackett/torrents?in=1&type=0&search=',
56
+ '<title>Access denied</title>'),
57
+ ('epublibre', 'https://epublibre.unblockit.cat/catalogo/index/0/nuevo/todos/sin/todos/--/ajax',
58
+ '<title>epublibre - catálogo</title>'),
59
+ ('ext', 'https://ext.to/latest/?order=age&sort=desc',
60
+ '<title>Download Latest Torrents - EXT Torrents</title>'),
61
+ ('extratorrent', 'https://extratorrent.st/search/?srt=added&order=desc&search=720p&new=1&x=0&y=0',
62
+ 'Page 1 - ExtraTorrent'),
63
+ ('idope', 'https://idope.se/browse.html', '<title>Recent Torrents</title>'),
64
+ ('limetorrents', 'https://limetorrents.unblockninja.com/latest100',
65
+ '<title>Latest 100 torrents - LimeTorrents</title>'),
66
+ ('privatehd', 'https://privatehd.to/api/v1/jackett/torrents?in=1&type=0&search=',
67
+ '<title>Access denied</title>'),
68
+ ('torrentcore', 'https://torrentcore.xyz/index', '<title>Torrent[CORE] - Torrent community.</title>'),
69
+ ('torrentqq223', 'https://torrentqq223.com/torrent/newest.html', 'https://torrentqq223.com/ads/'),
70
+ ('36dm', 'https://www.36dm.club/1.html', 'https://www.36dm.club/yesterday-1.html'),
71
+ ('erai-raws', 'https://www.erai-raws.info/feed/?type=magnet', '403 Forbidden'),
72
+ ('teamos', 'https://www.teamos.xyz/torrents/?filename=&freeleech=',
73
+ '<title>Log in | Team OS : Your Only Destination To Custom OS !!</title>'),
74
+ ('yts', 'https://yts.unblockninja.com/api/v2/list_movies.json?query_term=&limit=50&sort=date_added',
75
+ '{"movie_count":')
76
+ ]
77
+ for site_name, site_url, site_text in sites_get:
78
+ with self.subTest(msg=site_name):
79
+ res = self.app.post_json('/v1', {
80
+ "cmd": "request.get",
81
+ "url": site_url
82
+ })
83
+ asset_cloudflare_solution(self, res, site_url, site_text)
84
+
85
+ def test_v1_endpoint_request_post_cloudflare(self):
86
+ sites_post = [
87
+ ('nnmclub', 'https://nnmclub.to/forum/tracker.php', '<title>Трекер :: NNM-Club</title>',
88
+ 'prev_sd=0&prev_a=0&prev_my=0&prev_n=0&prev_shc=0&prev_shf=1&prev_sha=1&prev_shs=0&prev_shr=0&prev_sht=0&f%5B%5D=-1&o=1&s=2&tm=-1&shf=1&sha=1&ta=-1&sns=-1&sds=-1&nm=&pn=&submit=%CF%EE%E8%F1%EA')
89
+ ]
90
+
91
+ for site_name, site_url, site_text, post_data in sites_post:
92
+ with self.subTest(msg=site_name):
93
+ res = self.app.post_json('/v1', {
94
+ "cmd": "request.post",
95
+ "url": site_url,
96
+ "postData": post_data
97
+ })
98
+ asset_cloudflare_solution(self, res, site_url, site_text)
99
+
100
+
101
+ if __name__ == '__main__':
102
+ unittest.main()
src/undetected_chromedriver/__init__.py ADDED
@@ -0,0 +1,910 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+
5
+ 888 888 d8b
6
+ 888 888 Y8P
7
+ 888 888
8
+ .d8888b 88888b. 888d888 .d88b. 88888b.d88b. .d88b. .d88888 888d888 888 888 888 .d88b. 888d888
9
+ d88P" 888 "88b 888P" d88""88b 888 "888 "88b d8P Y8b d88" 888 888P" 888 888 888 d8P Y8b 888P"
10
+ 888 888 888 888 888 888 888 888 888 88888888 888 888 888 888 Y88 88P 88888888 888
11
+ Y88b. 888 888 888 Y88..88P 888 888 888 Y8b. Y88b 888 888 888 Y8bd8P Y8b. 888
12
+ "Y8888P 888 888 888 "Y88P" 888 888 888 "Y8888 "Y88888 888 888 Y88P "Y8888 888 88888888
13
+
14
+ by UltrafunkAmsterdam (https://github.com/ultrafunkamsterdam)
15
+
16
+ """
17
+ from __future__ import annotations
18
+
19
+
20
+ __version__ = "3.5.5"
21
+
22
+ import json
23
+ import logging
24
+ import os
25
+ import pathlib
26
+ import re
27
+ import shutil
28
+ import subprocess
29
+ import sys
30
+ import tempfile
31
+ import time
32
+ from weakref import finalize
33
+
34
+ import selenium.webdriver.chrome.service
35
+ import selenium.webdriver.chrome.webdriver
36
+ from selenium.webdriver.common.by import By
37
+ import selenium.webdriver.chromium.service
38
+ import selenium.webdriver.remote.command
39
+ import selenium.webdriver.remote.webdriver
40
+
41
+ from .cdp import CDP
42
+ from .dprocess import start_detached
43
+ from .options import ChromeOptions
44
+ from .patcher import IS_POSIX
45
+ from .patcher import Patcher
46
+ from .reactor import Reactor
47
+ from .webelement import UCWebElement
48
+ from .webelement import WebElement
49
+
50
+
51
+ __all__ = (
52
+ "Chrome",
53
+ "ChromeOptions",
54
+ "Patcher",
55
+ "Reactor",
56
+ "CDP",
57
+ "find_chrome_executable",
58
+ )
59
+
60
+ logger = logging.getLogger("uc")
61
+ logger.setLevel(logging.getLogger().getEffectiveLevel())
62
+
63
+
64
+ class Chrome(selenium.webdriver.chrome.webdriver.WebDriver):
65
+ """
66
+
67
+ Controls the ChromeDriver and allows you to drive the browser.
68
+
69
+ The webdriver file will be downloaded by this module automatically,
70
+ you do not need to specify this. however, you may if you wish.
71
+
72
+ Attributes
73
+ ----------
74
+
75
+ Methods
76
+ -------
77
+
78
+ reconnect()
79
+
80
+ this can be useful in case of heavy detection methods
81
+ -stops the chromedriver service which runs in the background
82
+ -starts the chromedriver service which runs in the background
83
+ -recreate session
84
+
85
+
86
+ start_session(capabilities=None, browser_profile=None)
87
+
88
+ differentiates from the regular method in that it does not
89
+ require a capabilities argument. The capabilities are automatically
90
+ recreated from the options at creation time.
91
+
92
+ --------------------------------------------------------------------------
93
+ NOTE:
94
+ Chrome has everything included to work out of the box.
95
+ it does not `need` customizations.
96
+ any customizations MAY lead to trigger bot migitation systems.
97
+
98
+ --------------------------------------------------------------------------
99
+ """
100
+
101
+ _instances = set()
102
+ session_id = None
103
+ debug = False
104
+
105
+ def __init__(
106
+ self,
107
+ options=None,
108
+ user_data_dir=None,
109
+ driver_executable_path=None,
110
+ browser_executable_path=None,
111
+ port=0,
112
+ enable_cdp_events=False,
113
+ # service_args=None,
114
+ # service_creationflags=None,
115
+ desired_capabilities=None,
116
+ advanced_elements=False,
117
+ # service_log_path=None,
118
+ keep_alive=True,
119
+ log_level=0,
120
+ headless=False,
121
+ version_main=None,
122
+ patcher_force_close=False,
123
+ suppress_welcome=True,
124
+ use_subprocess=False,
125
+ debug=False,
126
+ no_sandbox=True,
127
+ windows_headless=False,
128
+ user_multi_procs: bool = False,
129
+ **kw,
130
+ ):
131
+ """
132
+ Creates a new instance of the chrome driver.
133
+
134
+ Starts the service and then creates new instance of chrome driver.
135
+
136
+ Parameters
137
+ ----------
138
+
139
+ options: ChromeOptions, optional, default: None - automatic useful defaults
140
+ this takes an instance of ChromeOptions, mainly to customize browser behavior.
141
+ anything other dan the default, for example extensions or startup options
142
+ are not supported in case of failure, and can probably lowers your undetectability.
143
+
144
+
145
+ user_data_dir: str , optional, default: None (creates temp profile)
146
+ if user_data_dir is a path to a valid chrome profile directory, use it,
147
+ and turn off automatic removal mechanism at exit.
148
+
149
+ driver_executable_path: str, optional, default: None(=downloads and patches new binary)
150
+
151
+ browser_executable_path: str, optional, default: None - use find_chrome_executable
152
+ Path to the browser executable.
153
+ If not specified, make sure the executable's folder is in $PATH
154
+
155
+ port: int, optional, default: 0
156
+ port to be used by the chromedriver executable, this is NOT the debugger port.
157
+ leave it at 0 unless you know what you are doing.
158
+ the default value of 0 automatically picks an available port.
159
+
160
+ enable_cdp_events: bool, default: False
161
+ :: currently for chrome only
162
+ this enables the handling of wire messages
163
+ when enabled, you can subscribe to CDP events by using:
164
+
165
+ driver.add_cdp_listener("Network.dataReceived", yourcallback)
166
+ # yourcallback is an callable which accepts exactly 1 dict as parameter
167
+
168
+
169
+ service_args: list of str, optional, default: None
170
+ arguments to pass to the driver service
171
+
172
+ desired_capabilities: dict, optional, default: None - auto from config
173
+ Dictionary object with non-browser specific capabilities only, such as "item" or "loggingPref".
174
+
175
+ advanced_elements: bool, optional, default: False
176
+ makes it easier to recognize elements like you know them from html/browser inspection, especially when working
177
+ in an interactive environment
178
+
179
+ default webelement repr:
180
+ <selenium.webdriver.remote.webelement.WebElement (session="85ff0f671512fa535630e71ee951b1f2", element="6357cb55-92c3-4c0f-9416-b174f9c1b8c4")>
181
+
182
+ advanced webelement repr
183
+ <WebElement(<a class="mobile-show-inline-block mc-update-infos init-ok" href="#" id="main-cat-switcher-mobile">)>
184
+
185
+ note: when retrieving large amounts of elements ( example: find_elements_by_tag("*") ) and print them, it does take a little more time.
186
+
187
+
188
+ service_log_path: str, optional, default: None
189
+ path to log information from the driver.
190
+
191
+ keep_alive: bool, optional, default: True
192
+ Whether to configure ChromeRemoteConnection to use HTTP keep-alive.
193
+
194
+ log_level: int, optional, default: adapts to python global log level
195
+
196
+ headless: bool, optional, default: False
197
+ can also be specified in the options instance.
198
+ Specify whether you want to use the browser in headless mode.
199
+ warning: this lowers undetectability and not fully supported.
200
+
201
+ version_main: int, optional, default: None (=auto)
202
+ if you, for god knows whatever reason, use
203
+ an older version of Chrome. You can specify it's full rounded version number
204
+ here. Example: 87 for all versions of 87
205
+
206
+ patcher_force_close: bool, optional, default: False
207
+ instructs the patcher to do whatever it can to access the chromedriver binary
208
+ if the file is locked, it will force shutdown all instances.
209
+ setting it is not recommended, unless you know the implications and think
210
+ you might need it.
211
+
212
+ suppress_welcome: bool, optional , default: True
213
+ a "welcome" alert might show up on *nix-like systems asking whether you want to set
214
+ chrome as your default browser, and if you want to send even more data to google.
215
+ now, in case you are nag-fetishist, or a diagnostics data feeder to google, you can set this to False.
216
+ Note: if you don't handle the nag screen in time, the browser loses it's connection and throws an Exception.
217
+
218
+ use_subprocess: bool, optional , default: True,
219
+
220
+ False (the default) makes sure Chrome will get it's own process (so no subprocess of chromedriver.exe or python
221
+ This fixes a LOT of issues, like multithreaded run, but mst importantly. shutting corectly after
222
+ program exits or using .quit()
223
+ you should be knowing what you're doing, and know how python works.
224
+
225
+ unfortunately, there is always an edge case in which one would like to write an single script with the only contents being:
226
+ --start script--
227
+ import undetected_chromedriver as uc
228
+ d = uc.Chrome()
229
+ d.get('https://somesite/')
230
+ ---end script --
231
+
232
+ and will be greeted with an error, since the program exists before chrome has a change to launch.
233
+ in that case you can set this to `True`. The browser will start via subprocess, and will keep running most of times.
234
+ ! setting it to True comes with NO support when being detected. !
235
+
236
+ no_sandbox: bool, optional, default=True
237
+ uses the --no-sandbox option, and additionally does suppress the "unsecure option" status bar
238
+ this option has a default of True since many people seem to run this as root (....) , and chrome does not start
239
+ when running as root without using --no-sandbox flag.
240
+
241
+ user_multi_procs:
242
+ set to true when you are using multithreads/multiprocessing
243
+ ensures not all processes are trying to modify a binary which is in use by another.
244
+ for this to work. YOU MUST HAVE AT LEAST 1 UNDETECTED_CHROMEDRIVER BINARY IN YOUR ROAMING DATA FOLDER.
245
+ this requirement can be easily satisfied, by just running this program "normal" and close/kill it.
246
+
247
+
248
+ """
249
+
250
+ finalize(self, self._ensure_close, self)
251
+ self.debug = debug
252
+ self.patcher = Patcher(
253
+ executable_path=driver_executable_path,
254
+ force=patcher_force_close,
255
+ version_main=version_main,
256
+ user_multi_procs=user_multi_procs,
257
+ )
258
+ # self.patcher.auto(user_multiprocess = user_multi_num_procs)
259
+ self.patcher.auto()
260
+
261
+ # self.patcher = patcher
262
+ if not options:
263
+ options = ChromeOptions()
264
+
265
+ try:
266
+ if hasattr(options, "_session") and options._session is not None:
267
+ # prevent reuse of options,
268
+ # as it just appends arguments, not replace them
269
+ # you'll get conflicts starting chrome
270
+ raise RuntimeError("you cannot reuse the ChromeOptions object")
271
+ except AttributeError:
272
+ pass
273
+
274
+ options._session = self
275
+
276
+ if not options.debugger_address:
277
+ debug_port = (
278
+ port
279
+ if port != 0
280
+ else selenium.webdriver.common.service.utils.free_port()
281
+ )
282
+ debug_host = "127.0.0.1"
283
+ options.debugger_address = "%s:%d" % (debug_host, debug_port)
284
+ else:
285
+ debug_host, debug_port = options.debugger_address.split(":")
286
+ debug_port = int(debug_port)
287
+
288
+ if enable_cdp_events:
289
+ options.set_capability(
290
+ "goog:loggingPrefs", {"performance": "ALL", "browser": "ALL"}
291
+ )
292
+
293
+ options.add_argument("--remote-debugging-host=%s" % debug_host)
294
+ options.add_argument("--remote-debugging-port=%s" % debug_port)
295
+
296
+ if user_data_dir:
297
+ options.add_argument("--user-data-dir=%s" % user_data_dir)
298
+
299
+ language, keep_user_data_dir = None, bool(user_data_dir)
300
+
301
+ # see if a custom user profile is specified in options
302
+ for arg in options.arguments:
303
+
304
+ if any([_ in arg for _ in ("--headless", "headless")]):
305
+ options.arguments.remove(arg)
306
+ options.headless = True
307
+
308
+ if "lang" in arg:
309
+ m = re.search("(?:--)?lang(?:[ =])?(.*)", arg)
310
+ try:
311
+ language = m[1]
312
+ except IndexError:
313
+ logger.debug("will set the language to en-US,en;q=0.9")
314
+ language = "en-US,en;q=0.9"
315
+
316
+ if "user-data-dir" in arg:
317
+ m = re.search("(?:--)?user-data-dir(?:[ =])?(.*)", arg)
318
+ try:
319
+ user_data_dir = m[1]
320
+ logger.debug(
321
+ "user-data-dir found in user argument %s => %s" % (arg, m[1])
322
+ )
323
+ keep_user_data_dir = True
324
+
325
+ except IndexError:
326
+ logger.debug(
327
+ "no user data dir could be extracted from supplied argument %s "
328
+ % arg
329
+ )
330
+
331
+ if not user_data_dir:
332
+ # backward compatiblity
333
+ # check if an old uc.ChromeOptions is used, and extract the user data dir
334
+
335
+ if hasattr(options, "user_data_dir") and getattr(
336
+ options, "user_data_dir", None
337
+ ):
338
+ import warnings
339
+
340
+ warnings.warn(
341
+ "using ChromeOptions.user_data_dir might stop working in future versions."
342
+ "use uc.Chrome(user_data_dir='/xyz/some/data') in case you need existing profile folder"
343
+ )
344
+ options.add_argument("--user-data-dir=%s" % options.user_data_dir)
345
+ keep_user_data_dir = True
346
+ logger.debug(
347
+ "user_data_dir property found in options object: %s" % user_data_dir
348
+ )
349
+
350
+ else:
351
+ user_data_dir = os.path.normpath(tempfile.mkdtemp())
352
+ keep_user_data_dir = False
353
+ arg = "--user-data-dir=%s" % user_data_dir
354
+ options.add_argument(arg)
355
+ logger.debug(
356
+ "created a temporary folder in which the user-data (profile) will be stored during this\n"
357
+ "session, and added it to chrome startup arguments: %s" % arg
358
+ )
359
+
360
+ if not language:
361
+ try:
362
+ import locale
363
+
364
+ language = locale.getdefaultlocale()[0].replace("_", "-")
365
+ except Exception:
366
+ pass
367
+ if not language:
368
+ language = "en-US"
369
+
370
+ options.add_argument("--lang=%s" % language)
371
+
372
+ if not options.binary_location:
373
+ options.binary_location = (
374
+ browser_executable_path or find_chrome_executable()
375
+ )
376
+
377
+ if not options.binary_location or not \
378
+ pathlib.Path(options.binary_location).exists():
379
+ raise FileNotFoundError(
380
+ "\n---------------------\n"
381
+ "Could not determine browser executable."
382
+ "\n---------------------\n"
383
+ "Make sure your browser is installed in the default location (path).\n"
384
+ "If you are sure about the browser executable, you can specify it using\n"
385
+ "the `browser_executable_path='{}` parameter.\n\n"
386
+ .format("/path/to/browser/executable" if IS_POSIX else "c:/path/to/your/browser.exe")
387
+ )
388
+
389
+ self._delay = 3
390
+
391
+ self.user_data_dir = user_data_dir
392
+ self.keep_user_data_dir = keep_user_data_dir
393
+
394
+ if suppress_welcome:
395
+ options.arguments.extend(["--no-default-browser-check", "--no-first-run"])
396
+ if no_sandbox:
397
+ options.arguments.extend(["--no-sandbox", "--test-type"])
398
+
399
+ if headless or getattr(options, 'headless', None):
400
+ #workaround until a better checking is found
401
+ try:
402
+ v_main = int(self.patcher.version_main) if self.patcher.version_main else 108
403
+ if v_main < 108:
404
+ options.add_argument("--headless=chrome")
405
+ elif v_main >= 108:
406
+ options.add_argument("--headless=new")
407
+ except:
408
+ logger.warning("could not detect version_main."
409
+ "therefore, we are assuming it is chrome 108 or higher")
410
+ options.add_argument("--headless=new")
411
+
412
+ options.add_argument("--window-size=1920,1080")
413
+ options.add_argument("--start-maximized")
414
+ options.add_argument("--no-sandbox")
415
+ # fixes "could not connect to chrome" error when running
416
+ # on linux using privileged user like root (which i don't recommend)
417
+
418
+ options.add_argument(
419
+ "--log-level=%d" % log_level
420
+ or divmod(logging.getLogger().getEffectiveLevel(), 10)[0]
421
+ )
422
+
423
+ if hasattr(options, "handle_prefs"):
424
+ options.handle_prefs(user_data_dir)
425
+
426
+ # fix exit_type flag to prevent tab-restore nag
427
+ try:
428
+ with open(
429
+ os.path.join(user_data_dir, "Default/Preferences"),
430
+ encoding="latin1",
431
+ mode="r+",
432
+ ) as fs:
433
+ config = json.load(fs)
434
+ if config["profile"]["exit_type"] is not None:
435
+ # fixing the restore-tabs-nag
436
+ config["profile"]["exit_type"] = None
437
+ fs.seek(0, 0)
438
+ json.dump(config, fs)
439
+ fs.truncate() # the file might be shorter
440
+ logger.debug("fixed exit_type flag")
441
+ except Exception as e:
442
+ logger.debug("did not find a bad exit_type flag ")
443
+
444
+ self.options = options
445
+
446
+ if not desired_capabilities:
447
+ desired_capabilities = options.to_capabilities()
448
+
449
+ if not use_subprocess and not windows_headless:
450
+ self.browser_pid = start_detached(
451
+ options.binary_location, *options.arguments
452
+ )
453
+ else:
454
+ startupinfo = None
455
+ if os.name == 'nt' and windows_headless:
456
+ # STARTUPINFO() is Windows only
457
+ startupinfo = subprocess.STARTUPINFO()
458
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
459
+ browser = subprocess.Popen(
460
+ [options.binary_location, *options.arguments],
461
+ stdin=subprocess.PIPE,
462
+ stdout=subprocess.PIPE,
463
+ stderr=subprocess.PIPE,
464
+ close_fds=IS_POSIX,
465
+ startupinfo=startupinfo
466
+ )
467
+ self.browser_pid = browser.pid
468
+
469
+
470
+ service = selenium.webdriver.chromium.service.ChromiumService(
471
+ self.patcher.executable_path
472
+ )
473
+
474
+ super().__init__(
475
+ service=service,
476
+ options=options,
477
+ keep_alive=keep_alive,
478
+ )
479
+
480
+ self.reactor = None
481
+
482
+ if enable_cdp_events:
483
+ if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
484
+ logging.getLogger(
485
+ "selenium.webdriver.remote.remote_connection"
486
+ ).setLevel(20)
487
+ reactor = Reactor(self)
488
+ reactor.start()
489
+ self.reactor = reactor
490
+
491
+ if advanced_elements:
492
+ self._web_element_cls = UCWebElement
493
+ else:
494
+ self._web_element_cls = WebElement
495
+
496
+ if headless or getattr(options, 'headless', None):
497
+ self._configure_headless()
498
+
499
+ def _configure_headless(self):
500
+ orig_get = self.get
501
+ logger.info("setting properties for headless")
502
+
503
+ def get_wrapped(*args, **kwargs):
504
+ if self.execute_script("return navigator.webdriver"):
505
+ logger.info("patch navigator.webdriver")
506
+ self.execute_cdp_cmd(
507
+ "Page.addScriptToEvaluateOnNewDocument",
508
+ {
509
+ "source": """
510
+ Object.defineProperty(window, "navigator", {
511
+ value: new Proxy(navigator, {
512
+ has: (target, key) => (key === "webdriver" ? false : key in target),
513
+ get: (target, key) =>
514
+ key === "webdriver"
515
+ ? false
516
+ : typeof target[key] === "function"
517
+ ? target[key].bind(target)
518
+ : target[key],
519
+ }),
520
+ });
521
+ """
522
+ },
523
+ )
524
+
525
+ logger.info("patch user-agent string")
526
+ self.execute_cdp_cmd(
527
+ "Network.setUserAgentOverride",
528
+ {
529
+ "userAgent": self.execute_script(
530
+ "return navigator.userAgent"
531
+ ).replace("Headless", "")
532
+ },
533
+ )
534
+ self.execute_cdp_cmd(
535
+ "Page.addScriptToEvaluateOnNewDocument",
536
+ {
537
+ "source": """
538
+ Object.defineProperty(navigator, 'maxTouchPoints', {get: () => 1});
539
+ Object.defineProperty(navigator.connection, 'rtt', {get: () => 100});
540
+
541
+ // https://github.com/microlinkhq/browserless/blob/master/packages/goto/src/evasions/chrome-runtime.js
542
+ window.chrome = {
543
+ app: {
544
+ isInstalled: false,
545
+ InstallState: {
546
+ DISABLED: 'disabled',
547
+ INSTALLED: 'installed',
548
+ NOT_INSTALLED: 'not_installed'
549
+ },
550
+ RunningState: {
551
+ CANNOT_RUN: 'cannot_run',
552
+ READY_TO_RUN: 'ready_to_run',
553
+ RUNNING: 'running'
554
+ }
555
+ },
556
+ runtime: {
557
+ OnInstalledReason: {
558
+ CHROME_UPDATE: 'chrome_update',
559
+ INSTALL: 'install',
560
+ SHARED_MODULE_UPDATE: 'shared_module_update',
561
+ UPDATE: 'update'
562
+ },
563
+ OnRestartRequiredReason: {
564
+ APP_UPDATE: 'app_update',
565
+ OS_UPDATE: 'os_update',
566
+ PERIODIC: 'periodic'
567
+ },
568
+ PlatformArch: {
569
+ ARM: 'arm',
570
+ ARM64: 'arm64',
571
+ MIPS: 'mips',
572
+ MIPS64: 'mips64',
573
+ X86_32: 'x86-32',
574
+ X86_64: 'x86-64'
575
+ },
576
+ PlatformNaclArch: {
577
+ ARM: 'arm',
578
+ MIPS: 'mips',
579
+ MIPS64: 'mips64',
580
+ X86_32: 'x86-32',
581
+ X86_64: 'x86-64'
582
+ },
583
+ PlatformOs: {
584
+ ANDROID: 'android',
585
+ CROS: 'cros',
586
+ LINUX: 'linux',
587
+ MAC: 'mac',
588
+ OPENBSD: 'openbsd',
589
+ WIN: 'win'
590
+ },
591
+ RequestUpdateCheckStatus: {
592
+ NO_UPDATE: 'no_update',
593
+ THROTTLED: 'throttled',
594
+ UPDATE_AVAILABLE: 'update_available'
595
+ }
596
+ }
597
+ }
598
+
599
+ // https://github.com/microlinkhq/browserless/blob/master/packages/goto/src/evasions/navigator-permissions.js
600
+ if (!window.Notification) {
601
+ window.Notification = {
602
+ permission: 'denied'
603
+ }
604
+ }
605
+
606
+ const originalQuery = window.navigator.permissions.query
607
+ window.navigator.permissions.__proto__.query = parameters =>
608
+ parameters.name === 'notifications'
609
+ ? Promise.resolve({ state: window.Notification.permission })
610
+ : originalQuery(parameters)
611
+
612
+ const oldCall = Function.prototype.call
613
+ function call() {
614
+ return oldCall.apply(this, arguments)
615
+ }
616
+ Function.prototype.call = call
617
+
618
+ const nativeToStringFunctionString = Error.toString().replace(/Error/g, 'toString')
619
+ const oldToString = Function.prototype.toString
620
+
621
+ function functionToString() {
622
+ if (this === window.navigator.permissions.query) {
623
+ return 'function query() { [native code] }'
624
+ }
625
+ if (this === functionToString) {
626
+ return nativeToStringFunctionString
627
+ }
628
+ return oldCall.call(oldToString, this)
629
+ }
630
+ // eslint-disable-next-line
631
+ Function.prototype.toString = functionToString
632
+ """
633
+ },
634
+ )
635
+ return orig_get(*args, **kwargs)
636
+
637
+ self.get = get_wrapped
638
+
639
+ # def _get_cdc_props(self):
640
+ # return self.execute_script(
641
+ # """
642
+ # let objectToInspect = window,
643
+ # result = [];
644
+ # while(objectToInspect !== null)
645
+ # { result = result.concat(Object.getOwnPropertyNames(objectToInspect));
646
+ # objectToInspect = Object.getPrototypeOf(objectToInspect); }
647
+ #
648
+ # return result.filter(i => i.match(/^([a-zA-Z]){27}(Array|Promise|Symbol)$/ig))
649
+ # """
650
+ # )
651
+ #
652
+ # def _hook_remove_cdc_props(self):
653
+ # self.execute_cdp_cmd(
654
+ # "Page.addScriptToEvaluateOnNewDocument",
655
+ # {
656
+ # "source": """
657
+ # let objectToInspect = window,
658
+ # result = [];
659
+ # while(objectToInspect !== null)
660
+ # { result = result.concat(Object.getOwnPropertyNames(objectToInspect));
661
+ # objectToInspect = Object.getPrototypeOf(objectToInspect); }
662
+ # result.forEach(p => p.match(/^([a-zA-Z]){27}(Array|Promise|Symbol)$/ig)
663
+ # &&delete window[p]&&console.log('removed',p))
664
+ # """
665
+ # },
666
+ # )
667
+
668
+ def get(self, url):
669
+ # if self._get_cdc_props():
670
+ # self._hook_remove_cdc_props()
671
+ return super().get(url)
672
+
673
+ def add_cdp_listener(self, event_name, callback):
674
+ if (
675
+ self.reactor
676
+ and self.reactor is not None
677
+ and isinstance(self.reactor, Reactor)
678
+ ):
679
+ self.reactor.add_event_handler(event_name, callback)
680
+ return self.reactor.handlers
681
+ return False
682
+
683
+ def clear_cdp_listeners(self):
684
+ if self.reactor and isinstance(self.reactor, Reactor):
685
+ self.reactor.handlers.clear()
686
+
687
+ def window_new(self):
688
+ self.execute(
689
+ selenium.webdriver.remote.command.Command.NEW_WINDOW, {"type": "window"}
690
+ )
691
+
692
+ def tab_new(self, url: str):
693
+ """
694
+ this opens a url in a new tab.
695
+ apparently, that passes all tests directly!
696
+
697
+ Parameters
698
+ ----------
699
+ url
700
+
701
+ Returns
702
+ -------
703
+
704
+ """
705
+ if not hasattr(self, "cdp"):
706
+ from .cdp import CDP
707
+
708
+ cdp = CDP(self.options)
709
+ cdp.tab_new(url)
710
+
711
+ def reconnect(self, timeout=0.1):
712
+ try:
713
+ self.service.stop()
714
+ except Exception as e:
715
+ logger.debug(e)
716
+ time.sleep(timeout)
717
+ try:
718
+ self.service.start()
719
+ except Exception as e:
720
+ logger.debug(e)
721
+
722
+ try:
723
+ self.start_session()
724
+ except Exception as e:
725
+ logger.debug(e)
726
+
727
+ def start_session(self, capabilities=None, browser_profile=None):
728
+ if not capabilities:
729
+ capabilities = self.options.to_capabilities()
730
+ super().start_session(capabilities)
731
+ # super(Chrome, self).start_session(capabilities, browser_profile) # Original explicit call commented out
732
+
733
+ def find_elements_recursive(self, by, value):
734
+ """
735
+ find elements in all frames
736
+ this is a generator function, which is needed
737
+ since if it would return a list of elements, they
738
+ will be stale on arrival.
739
+ using generator, when the element is returned we are in the correct frame
740
+ to use it directly
741
+ Args:
742
+ by: By
743
+ value: str
744
+ Returns: Generator[webelement.WebElement]
745
+ """
746
+ def search_frame(f=None):
747
+ if not f:
748
+ # ensure we are on main content frame
749
+ self.switch_to.default_content()
750
+ else:
751
+ self.switch_to.frame(f)
752
+ for elem in self.find_elements(by, value):
753
+ yield elem
754
+ # switch back to main content, otherwise we will get StaleElementReferenceException
755
+ self.switch_to.default_content()
756
+
757
+ # search root frame
758
+ for elem in search_frame():
759
+ yield elem
760
+ # get iframes
761
+ frames = self.find_elements('css selector', 'iframe')
762
+
763
+ # search per frame
764
+ for f in frames:
765
+ for elem in search_frame(f):
766
+ yield elem
767
+
768
+ def quit(self):
769
+ try:
770
+ self.service.stop()
771
+ self.service.process.kill()
772
+ self.command_executor.close()
773
+ self.service.process.wait(5)
774
+ logger.debug("webdriver process ended")
775
+ except (AttributeError, RuntimeError, OSError):
776
+ pass
777
+ try:
778
+ self.reactor.event.set()
779
+ logger.debug("shutting down reactor")
780
+ except AttributeError:
781
+ pass
782
+ try:
783
+ os.kill(self.browser_pid, 15)
784
+ logger.debug("gracefully closed browser")
785
+ except Exception as e: # noqa
786
+ pass
787
+ if (
788
+ hasattr(self, "keep_user_data_dir")
789
+ and hasattr(self, "user_data_dir")
790
+ and not self.keep_user_data_dir
791
+ ):
792
+ for _ in range(5):
793
+ try:
794
+ shutil.rmtree(self.user_data_dir, ignore_errors=False)
795
+ except FileNotFoundError:
796
+ pass
797
+ except (RuntimeError, OSError, PermissionError) as e:
798
+ logger.debug(
799
+ "When removing the temp profile, a %s occured: %s\nretrying..."
800
+ % (e.__class__.__name__, e)
801
+ )
802
+ else:
803
+ logger.debug("successfully removed %s" % self.user_data_dir)
804
+ break
805
+
806
+ try:
807
+ time.sleep(0.1)
808
+ except OSError:
809
+ pass
810
+
811
+ # dereference patcher, so patcher can start cleaning up as well.
812
+ # this must come last, otherwise it will throw 'in use' errors
813
+ self.patcher = None
814
+
815
+ def __getattribute__(self, item):
816
+ if not super().__getattribute__("debug"):
817
+ return super().__getattribute__(item)
818
+ else:
819
+ import inspect
820
+
821
+ original = super().__getattribute__(item)
822
+ if inspect.ismethod(original) and not inspect.isclass(original):
823
+
824
+ def newfunc(*args, **kwargs):
825
+ logger.debug(
826
+ "calling %s with args %s and kwargs %s\n"
827
+ % (original.__qualname__, args, kwargs)
828
+ )
829
+ return original(*args, **kwargs)
830
+
831
+ return newfunc
832
+ return original
833
+
834
+ def __enter__(self):
835
+ return self
836
+
837
+ def __exit__(self, exc_type, exc_val, exc_tb):
838
+ self.service.stop()
839
+ time.sleep(self._delay)
840
+ self.service.start()
841
+ self.start_session()
842
+
843
+ def __hash__(self):
844
+ return hash(self.options.debugger_address)
845
+
846
+ def __dir__(self):
847
+ return object.__dir__(self)
848
+
849
+ def __del__(self):
850
+ try:
851
+ self.service.process.kill()
852
+ except: # noqa
853
+ pass
854
+ self.quit()
855
+
856
+ @classmethod
857
+ def _ensure_close(cls, self):
858
+ # needs to be a classmethod so finalize can find the reference
859
+ logger.info("ensuring close")
860
+ if (
861
+ hasattr(self, "service")
862
+ and hasattr(self.service, "process")
863
+ and hasattr(self.service.process, "kill")
864
+ ):
865
+ self.service.process.kill()
866
+
867
+
868
+ def find_chrome_executable():
869
+ """
870
+ Finds the chrome, chrome beta, chrome canary, chromium executable
871
+
872
+ Returns
873
+ -------
874
+ executable_path : str
875
+ the full file path to found executable
876
+
877
+ """
878
+ candidates = set()
879
+ if IS_POSIX:
880
+ for item in os.environ.get("PATH").split(os.pathsep):
881
+ for subitem in (
882
+ "google-chrome",
883
+ "chromium",
884
+ "chromium-browser",
885
+ "chrome",
886
+ "google-chrome-stable",
887
+ ):
888
+ candidates.add(os.sep.join((item, subitem)))
889
+ if "darwin" in sys.platform:
890
+ candidates.update(
891
+ [
892
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
893
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
894
+ ]
895
+ )
896
+ else:
897
+ for item in map(
898
+ os.environ.get,
899
+ ("PROGRAMFILES", "PROGRAMFILES(X86)", "LOCALAPPDATA", "PROGRAMW6432"),
900
+ ):
901
+ if item is not None:
902
+ for subitem in (
903
+ "Google/Chrome/Application",
904
+ ):
905
+ candidates.add(os.sep.join((item, subitem, "chrome.exe")))
906
+ for candidate in candidates:
907
+ logger.debug('checking if %s exists and is executable' % candidate)
908
+ if os.path.exists(candidate) and os.access(candidate, os.X_OK):
909
+ logger.debug('found! using %s' % candidate)
910
+ return os.path.normpath(candidate)
src/undetected_chromedriver/cdp.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # this module is part of undetected_chromedriver
3
+
4
+ import json
5
+ import logging
6
+
7
+ import requests
8
+ import websockets
9
+
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+
14
+ class CDPObject(dict):
15
+ def __init__(self, *a, **k):
16
+ super().__init__(*a, **k)
17
+ self.__dict__ = self
18
+ for k in self.__dict__:
19
+ if isinstance(self.__dict__[k], dict):
20
+ self.__dict__[k] = CDPObject(self.__dict__[k])
21
+ elif isinstance(self.__dict__[k], list):
22
+ for i in range(len(self.__dict__[k])):
23
+ if isinstance(self.__dict__[k][i], dict):
24
+ self.__dict__[k][i] = CDPObject(self)
25
+
26
+ def __repr__(self):
27
+ tpl = f"{self.__class__.__name__}(\n\t{{}}\n\t)"
28
+ return tpl.format("\n ".join(f"{k} = {v}" for k, v in self.items()))
29
+
30
+
31
+ class PageElement(CDPObject):
32
+ pass
33
+
34
+
35
+ class CDP:
36
+ log = logging.getLogger("CDP")
37
+
38
+ endpoints = CDPObject(
39
+ {
40
+ "json": "/json",
41
+ "protocol": "/json/protocol",
42
+ "list": "/json/list",
43
+ "new": "/json/new?{url}",
44
+ "activate": "/json/activate/{id}",
45
+ "close": "/json/close/{id}",
46
+ }
47
+ )
48
+
49
+ def __init__(self, options: "ChromeOptions"): # noqa
50
+ self.server_addr = "http://{0}:{1}".format(*options.debugger_address.split(":"))
51
+
52
+ self._reqid = 0
53
+ self._session = requests.Session()
54
+ self._last_resp = None
55
+ self._last_json = None
56
+
57
+ resp = self.get(self.endpoints.json) # noqa
58
+ self.sessionId = resp[0]["id"]
59
+ self.wsurl = resp[0]["webSocketDebuggerUrl"]
60
+
61
+ def tab_activate(self, id=None):
62
+ if not id:
63
+ active_tab = self.tab_list()[0]
64
+ id = active_tab.id # noqa
65
+ self.wsurl = active_tab.webSocketDebuggerUrl # noqa
66
+ return self.post(self.endpoints["activate"].format(id=id))
67
+
68
+ def tab_list(self):
69
+ retval = self.get(self.endpoints["list"])
70
+ return [PageElement(o) for o in retval]
71
+
72
+ def tab_new(self, url):
73
+ return self.post(self.endpoints["new"].format(url=url))
74
+
75
+ def tab_close_last_opened(self):
76
+ sessions = self.tab_list()
77
+ opentabs = [s for s in sessions if s["type"] == "page"]
78
+ return self.post(self.endpoints["close"].format(id=opentabs[-1]["id"]))
79
+
80
+ async def send(self, method: str, params: dict):
81
+ self._reqid += 1
82
+ async with websockets.connect(self.wsurl) as ws:
83
+ await ws.send(
84
+ json.dumps({"method": method, "params": params, "id": self._reqid})
85
+ )
86
+ self._last_resp = await ws.recv()
87
+ self._last_json = json.loads(self._last_resp)
88
+ self.log.info(self._last_json)
89
+
90
+ def get(self, uri):
91
+ resp = self._session.get(self.server_addr + uri)
92
+ try:
93
+ self._last_resp = resp
94
+ self._last_json = resp.json()
95
+ except Exception:
96
+ return
97
+ else:
98
+ return self._last_json
99
+
100
+ def post(self, uri, data: dict = None):
101
+ if not data:
102
+ data = {}
103
+ resp = self._session.post(self.server_addr + uri, json=data)
104
+ try:
105
+ self._last_resp = resp
106
+ self._last_json = resp.json()
107
+ except Exception:
108
+ return self._last_resp
109
+
110
+ @property
111
+ def last_json(self):
112
+ return self._last_json
src/undetected_chromedriver/devtool.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from collections.abc import Mapping
3
+ from collections.abc import Sequence
4
+ from functools import wraps
5
+ import os
6
+ import logging
7
+ import threading
8
+ import time
9
+ import traceback
10
+ from typing import Any
11
+ from typing import Awaitable
12
+ from typing import Callable
13
+ from typing import List
14
+ from typing import Optional
15
+
16
+
17
+ class Structure(dict):
18
+ """
19
+ This is a dict-like object structure, which you should subclass
20
+ Only properties defined in the class context are used on initialization.
21
+
22
+ See example
23
+ """
24
+
25
+ _store = {}
26
+
27
+ def __init__(self, *a, **kw):
28
+ """
29
+ Instantiate a new instance.
30
+
31
+ :param a:
32
+ :param kw:
33
+ """
34
+
35
+ super().__init__()
36
+
37
+ # auxiliar dict
38
+ d = dict(*a, **kw)
39
+ for k, v in d.items():
40
+ if isinstance(v, Mapping):
41
+ self[k] = self.__class__(v)
42
+ elif isinstance(v, Sequence) and not isinstance(v, (str, bytes)):
43
+ self[k] = [self.__class__(i) for i in v]
44
+ else:
45
+ self[k] = v
46
+ super().__setattr__("__dict__", self)
47
+
48
+ def __getattr__(self, item):
49
+ return getattr(super(), item)
50
+
51
+ def __getitem__(self, item):
52
+ return super().__getitem__(item)
53
+
54
+ def __setattr__(self, key, value):
55
+ self.__setitem__(key, value)
56
+
57
+ def __setitem__(self, key, value):
58
+ super().__setitem__(key, value)
59
+
60
+ def update(self, *a, **kw):
61
+ super().update(*a, **kw)
62
+
63
+ def __eq__(self, other):
64
+ return frozenset(other.items()) == frozenset(self.items())
65
+
66
+ def __hash__(self):
67
+ return hash(frozenset(self.items()))
68
+
69
+ @classmethod
70
+ def __init_subclass__(cls, **kwargs):
71
+ cls._store = {}
72
+
73
+ def _normalize_strings(self):
74
+ for k, v in self.copy().items():
75
+ if isinstance(v, (str)):
76
+ self[k] = v.strip()
77
+
78
+
79
+ def timeout(seconds=3, on_timeout: Optional[Callable[[callable], Any]] = None):
80
+ def wrapper(func):
81
+ @wraps(func)
82
+ def wrapped(*args, **kwargs):
83
+ def function_reached_timeout():
84
+ if on_timeout:
85
+ on_timeout(func)
86
+ else:
87
+ raise TimeoutError("function call timed out")
88
+
89
+ t = threading.Timer(interval=seconds, function=function_reached_timeout)
90
+ t.start()
91
+ try:
92
+ return func(*args, **kwargs)
93
+ except:
94
+ t.cancel()
95
+ raise
96
+ finally:
97
+ t.cancel()
98
+
99
+ return wrapped
100
+
101
+ return wrapper
102
+
103
+
104
+ def test():
105
+ import sys, os
106
+
107
+ sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
108
+ import undetected_chromedriver as uc
109
+ import threading
110
+
111
+ def collector(
112
+ driver: uc.Chrome,
113
+ stop_event: threading.Event,
114
+ on_event_coro: Optional[Callable[[List[str]], Awaitable[Any]]] = None,
115
+ listen_events: Sequence = ("browser", "network", "performance"),
116
+ ):
117
+ def threaded(driver, stop_event, on_event_coro):
118
+ async def _ensure_service_started():
119
+ while (
120
+ getattr(driver, "service", False)
121
+ and getattr(driver.service, "process", False)
122
+ and driver.service.process.poll()
123
+ ):
124
+ print("waiting for driver service to come back on")
125
+ await asyncio.sleep(0.05)
126
+ # await asyncio.sleep(driver._delay or .25)
127
+
128
+ async def get_log_lines(typ):
129
+ await _ensure_service_started()
130
+ return driver.get_log(typ)
131
+
132
+ async def looper():
133
+ while not stop_event.is_set():
134
+ log_lines = []
135
+ try:
136
+ for _ in listen_events:
137
+ try:
138
+ log_lines += await get_log_lines(_)
139
+ except:
140
+ if logging.getLogger().getEffectiveLevel() <= 10:
141
+ traceback.print_exc()
142
+ continue
143
+ if log_lines and on_event_coro:
144
+ await on_event_coro(log_lines)
145
+ except Exception as e:
146
+ if logging.getLogger().getEffectiveLevel() <= 10:
147
+ traceback.print_exc()
148
+
149
+ loop = asyncio.new_event_loop()
150
+ asyncio.set_event_loop(loop)
151
+ loop.run_until_complete(looper())
152
+
153
+ t = threading.Thread(target=threaded, args=(driver, stop_event, on_event_coro))
154
+ t.start()
155
+
156
+ async def on_event(data):
157
+ print("on_event")
158
+ print("data:", data)
159
+
160
+ def func_called(fn):
161
+ def wrapped(*args, **kwargs):
162
+ print(
163
+ "func called! %s (args: %s, kwargs: %s)" % (fn.__name__, args, kwargs)
164
+ )
165
+ while driver.service.process and driver.service.process.poll() is not None:
166
+ time.sleep(0.1)
167
+ res = fn(*args, **kwargs)
168
+ print("func completed! (result: %s)" % res)
169
+ return res
170
+
171
+ return wrapped
172
+
173
+ logging.basicConfig(level=10)
174
+
175
+ options = uc.ChromeOptions()
176
+ options.set_capability(
177
+ "goog:loggingPrefs", {"performance": "ALL", "browser": "ALL", "network": "ALL"}
178
+ )
179
+
180
+ driver = uc.Chrome(version_main=96, options=options)
181
+
182
+ # driver.command_executor._request = timeout(seconds=1)(driver.command_executor._request)
183
+ driver.command_executor._request = func_called(driver.command_executor._request)
184
+ collector_stop = threading.Event()
185
+ collector(driver, collector_stop, on_event)
186
+
187
+ driver.get("https://nowsecure.nl")
188
+
189
+ time.sleep(10)
190
+
191
+ if os.name == "nt":
192
+ driver.close()
193
+ driver.quit()
src/undetected_chromedriver/dprocess.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import atexit
2
+ import logging
3
+ import multiprocessing
4
+ import os
5
+ import platform
6
+ import signal
7
+ from subprocess import PIPE
8
+ from subprocess import Popen
9
+ import sys
10
+
11
+
12
+ CREATE_NEW_PROCESS_GROUP = 0x00000200
13
+ DETACHED_PROCESS = 0x00000008
14
+
15
+ REGISTERED = []
16
+
17
+
18
+ def start_detached(executable, *args):
19
+ """
20
+ Starts a fully independent subprocess (with no parent)
21
+ :param executable: executable
22
+ :param args: arguments to the executable, eg: ['--param1_key=param1_val', '-vvv' ...]
23
+ :return: pid of the grandchild process
24
+ """
25
+
26
+ # create pipe
27
+ reader, writer = multiprocessing.Pipe(False)
28
+
29
+ # do not keep reference
30
+ process = multiprocessing.Process(
31
+ target=_start_detached,
32
+ args=(executable, *args),
33
+ kwargs={"writer": writer},
34
+ daemon=True,
35
+ )
36
+ process.start()
37
+ process.join()
38
+ # receive pid from pipe
39
+ pid = reader.recv()
40
+ REGISTERED.append(pid)
41
+ # close pipes
42
+ writer.close()
43
+ reader.close()
44
+ process.close()
45
+
46
+ return pid
47
+
48
+
49
+ def _start_detached(executable, *args, writer: multiprocessing.Pipe = None):
50
+ # configure launch
51
+ kwargs = {}
52
+ if platform.system() == "Windows":
53
+ kwargs.update(creationflags=DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP)
54
+ elif sys.version_info < (3, 2):
55
+ # assume posix
56
+ kwargs.update(preexec_fn=os.setsid)
57
+ else: # Python 3.2+ and Unix
58
+ kwargs.update(start_new_session=True)
59
+
60
+ # run
61
+ p = Popen([executable, *args], stdin=PIPE, stdout=PIPE, stderr=PIPE, **kwargs)
62
+
63
+ # send pid to pipe
64
+ writer.send(p.pid)
65
+ sys.exit()
66
+
67
+
68
+ def _cleanup():
69
+ for pid in REGISTERED:
70
+ try:
71
+ logging.getLogger(__name__).debug("cleaning up pid %d " % pid)
72
+ os.kill(pid, signal.SIGTERM)
73
+ except: # noqa
74
+ pass
75
+
76
+
77
+ atexit.register(_cleanup)
src/undetected_chromedriver/options.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # this module is part of undetected_chromedriver
3
+
4
+
5
+ import json
6
+ import os
7
+
8
+ from selenium.webdriver.chromium.options import ChromiumOptions as _ChromiumOptions
9
+
10
+
11
+ class ChromeOptions(_ChromiumOptions):
12
+ _session = None
13
+ _user_data_dir = None
14
+
15
+ @property
16
+ def user_data_dir(self):
17
+ return self._user_data_dir
18
+
19
+ @user_data_dir.setter
20
+ def user_data_dir(self, path: str):
21
+ """
22
+ Sets the browser profile folder to use, or creates a new profile
23
+ at given <path>.
24
+
25
+ Parameters
26
+ ----------
27
+ path: str
28
+ the path to a chrome profile folder
29
+ if it does not exist, a new profile will be created at given location
30
+ """
31
+ apath = os.path.abspath(path)
32
+ self._user_data_dir = os.path.normpath(apath)
33
+
34
+ @staticmethod
35
+ def _undot_key(key, value):
36
+ """turn a (dotted key, value) into a proper nested dict"""
37
+ if "." in key:
38
+ key, rest = key.split(".", 1)
39
+ value = ChromeOptions._undot_key(rest, value)
40
+ return {key: value}
41
+
42
+ @staticmethod
43
+ def _merge_nested(a, b):
44
+ """
45
+ merges b into a
46
+ leaf values in a are overwritten with values from b
47
+ """
48
+ for key in b:
49
+ if key in a:
50
+ if isinstance(a[key], dict) and isinstance(b[key], dict):
51
+ ChromeOptions._merge_nested(a[key], b[key])
52
+ continue
53
+ a[key] = b[key]
54
+ return a
55
+
56
+ def handle_prefs(self, user_data_dir):
57
+ prefs = self.experimental_options.get("prefs")
58
+ if prefs:
59
+ user_data_dir = user_data_dir or self._user_data_dir
60
+ default_path = os.path.join(user_data_dir, "Default")
61
+ os.makedirs(default_path, exist_ok=True)
62
+
63
+ # undot prefs dict keys
64
+ undot_prefs = {}
65
+ for key, value in prefs.items():
66
+ undot_prefs = self._merge_nested(
67
+ undot_prefs, self._undot_key(key, value)
68
+ )
69
+
70
+ prefs_file = os.path.join(default_path, "Preferences")
71
+ if os.path.exists(prefs_file):
72
+ with open(prefs_file, encoding="latin1", mode="r") as f:
73
+ undot_prefs = self._merge_nested(json.load(f), undot_prefs)
74
+
75
+ with open(prefs_file, encoding="latin1", mode="w") as f:
76
+ json.dump(undot_prefs, f)
77
+
78
+ # remove the experimental_options to avoid an error
79
+ del self._experimental_options["prefs"]
80
+
81
+ @classmethod
82
+ def from_options(cls, options):
83
+ o = cls()
84
+ o.__dict__.update(options.__dict__)
85
+ return o
src/undetected_chromedriver/patcher.py ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # this module is part of undetected_chromedriver
3
+
4
+ from packaging.version import Version as LooseVersion
5
+ import io
6
+ import json
7
+ import logging
8
+ import os
9
+ import pathlib
10
+ import platform
11
+ import random
12
+ import re
13
+ import shutil
14
+ import string
15
+ import subprocess
16
+ import sys
17
+ import time
18
+ from urllib.request import urlopen
19
+ from urllib.request import urlretrieve
20
+ import zipfile
21
+ from multiprocessing import Lock
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ IS_POSIX = sys.platform.startswith(("darwin", "cygwin", "linux", "linux2", "freebsd"))
26
+
27
+
28
+ class Patcher(object):
29
+ lock = Lock()
30
+ exe_name = "chromedriver%s"
31
+
32
+ platform = sys.platform
33
+ if platform.endswith("win32"):
34
+ d = "~/appdata/roaming/undetected_chromedriver"
35
+ elif "LAMBDA_TASK_ROOT" in os.environ:
36
+ d = "/tmp/undetected_chromedriver"
37
+ elif platform.startswith(("linux", "linux2")):
38
+ d = "~/.local/share/undetected_chromedriver"
39
+ elif platform.endswith("darwin"):
40
+ d = "~/Library/Application Support/undetected_chromedriver"
41
+ else:
42
+ d = "~/.undetected_chromedriver"
43
+ data_path = os.path.abspath(os.path.expanduser(d))
44
+
45
+ def __init__(
46
+ self,
47
+ executable_path=None,
48
+ force=False,
49
+ version_main: int = 0,
50
+ user_multi_procs=False,
51
+ ):
52
+ """
53
+ Args:
54
+ executable_path: None = automatic
55
+ a full file path to the chromedriver executable
56
+ force: False
57
+ terminate processes which are holding lock
58
+ version_main: 0 = auto
59
+ specify main chrome version (rounded, ex: 82)
60
+ """
61
+ self.force = force
62
+ self._custom_exe_path = False
63
+ prefix = "undetected"
64
+ self.user_multi_procs = user_multi_procs
65
+
66
+ try:
67
+ # Try to convert version_main into an integer
68
+ version_main_int = int(version_main)
69
+ # check if version_main_int is less than or equal to e.g 114
70
+ self.is_old_chromedriver = version_main and version_main_int <= 114
71
+ except (ValueError,TypeError):
72
+ # Check not running inside Docker
73
+ if not os.path.exists("/app/chromedriver"):
74
+ # If the conversion fails, log an error message
75
+ logging.info("version_main cannot be converted to an integer")
76
+ # Set self.is_old_chromedriver to False if the conversion fails
77
+ self.is_old_chromedriver = False
78
+
79
+ # Needs to be called before self.exe_name is accessed
80
+ self._set_platform_name()
81
+
82
+ if not os.path.exists(self.data_path):
83
+ os.makedirs(self.data_path, exist_ok=True)
84
+
85
+ if not executable_path:
86
+ if sys.platform.startswith("freebsd"):
87
+ self.executable_path = os.path.join(
88
+ self.data_path, self.exe_name
89
+ )
90
+ else:
91
+ self.executable_path = os.path.join(
92
+ self.data_path, "_".join([prefix, self.exe_name])
93
+ )
94
+
95
+ if not IS_POSIX:
96
+ if executable_path:
97
+ if not executable_path[-4:] == ".exe":
98
+ executable_path += ".exe"
99
+
100
+ self.zip_path = os.path.join(self.data_path, prefix)
101
+
102
+ if not executable_path:
103
+ if not self.user_multi_procs:
104
+ self.executable_path = os.path.abspath(
105
+ os.path.join(".", self.executable_path)
106
+ )
107
+
108
+ if executable_path:
109
+ self._custom_exe_path = True
110
+ self.executable_path = executable_path
111
+
112
+ # Set the correct repository to download the Chromedriver from
113
+ if self.is_old_chromedriver:
114
+ self.url_repo = "https://chromedriver.storage.googleapis.com"
115
+ else:
116
+ self.url_repo = "https://googlechromelabs.github.io/chrome-for-testing"
117
+
118
+ self.version_main = version_main
119
+ self.version_full = None
120
+
121
+ def _set_platform_name(self):
122
+ """
123
+ Set the platform and exe name based on the platform undetected_chromedriver is running on
124
+ in order to download the correct chromedriver.
125
+ """
126
+ if self.platform.endswith("win32"):
127
+ self.platform_name = "win32"
128
+ self.exe_name %= ".exe"
129
+ if self.platform.endswith(("linux", "linux2")):
130
+ self.platform_name = "linux64"
131
+ self.exe_name %= ""
132
+ if self.platform.endswith("darwin"):
133
+ if self.is_old_chromedriver:
134
+ self.platform_name = "mac64"
135
+ else:
136
+ self.platform_name = "mac-x64"
137
+ self.exe_name %= ""
138
+ if self.platform.startswith("freebsd"):
139
+ self.platform_name = "freebsd"
140
+ self.exe_name %= ""
141
+
142
+ def auto(self, executable_path=None, force=False, version_main=None, _=None):
143
+ """
144
+
145
+ Args:
146
+ executable_path:
147
+ force:
148
+ version_main:
149
+
150
+ Returns:
151
+
152
+ """
153
+ p = pathlib.Path(self.data_path)
154
+ if self.user_multi_procs:
155
+ with Lock():
156
+ files = list(p.rglob("*chromedriver*"))
157
+ most_recent = max(files, key=lambda f: f.stat().st_mtime)
158
+ files.remove(most_recent)
159
+ list(map(lambda f: f.unlink(), files))
160
+ if self.is_binary_patched(most_recent):
161
+ self.executable_path = str(most_recent)
162
+ return True
163
+
164
+ if executable_path:
165
+ self.executable_path = executable_path
166
+ self._custom_exe_path = True
167
+
168
+ if self._custom_exe_path:
169
+ ispatched = self.is_binary_patched(self.executable_path)
170
+ if not ispatched:
171
+ return self.patch_exe()
172
+ else:
173
+ return
174
+
175
+ if version_main:
176
+ self.version_main = version_main
177
+ if force is True:
178
+ self.force = force
179
+
180
+
181
+ if self.platform_name == "freebsd":
182
+ chromedriver_path = shutil.which("chromedriver")
183
+
184
+ if not os.path.isfile(chromedriver_path) or not os.access(chromedriver_path, os.X_OK):
185
+ logging.error("Chromedriver not installed!")
186
+ return
187
+
188
+ version_path = os.path.join(os.path.dirname(self.executable_path), "version.txt")
189
+
190
+ process = os.popen(f'"{chromedriver_path}" --version')
191
+ chromedriver_version = process.read().split(' ')[1].split(' ')[0]
192
+ process.close()
193
+
194
+ current_version = None
195
+ if os.path.isfile(version_path) or os.access(version_path, os.X_OK):
196
+ with open(version_path, 'r') as f:
197
+ current_version = f.read()
198
+
199
+ if current_version != chromedriver_version:
200
+ logging.info("Copying chromedriver executable...")
201
+ shutil.copy(chromedriver_path, self.executable_path)
202
+ os.chmod(self.executable_path, 0o755)
203
+
204
+ with open(version_path, 'w') as f:
205
+ f.write(chromedriver_version)
206
+
207
+ logging.info("Chromedriver executable copied!")
208
+ else:
209
+ try:
210
+ os.unlink(self.executable_path)
211
+ except PermissionError:
212
+ if self.force:
213
+ self.force_kill_instances(self.executable_path)
214
+ return self.auto(force=not self.force)
215
+ try:
216
+ if self.is_binary_patched():
217
+ # assumes already running AND patched
218
+ return True
219
+ except PermissionError:
220
+ pass
221
+ # return False
222
+ except FileNotFoundError:
223
+ pass
224
+
225
+ release = self.fetch_release_number()
226
+ self.version_main = release.major
227
+ self.version_full = release
228
+ self.unzip_package(self.fetch_package())
229
+
230
+ return self.patch()
231
+
232
+ def driver_binary_in_use(self, path: str = None) -> bool:
233
+ """
234
+ naive test to check if a found chromedriver binary is
235
+ currently in use
236
+
237
+ Args:
238
+ path: a string or PathLike object to the binary to check.
239
+ if not specified, we check use this object's executable_path
240
+ """
241
+ if not path:
242
+ path = self.executable_path
243
+ p = pathlib.Path(path)
244
+
245
+ if not p.exists():
246
+ raise OSError("file does not exist: %s" % p)
247
+ try:
248
+ with open(p, mode="a+b") as fs:
249
+ exc = []
250
+ try:
251
+
252
+ fs.seek(0, 0)
253
+ except PermissionError as e:
254
+ exc.append(e) # since some systems apprently allow seeking
255
+ # we conduct another test
256
+ try:
257
+ fs.readline()
258
+ except PermissionError as e:
259
+ exc.append(e)
260
+
261
+ if exc:
262
+
263
+ return True
264
+ return False
265
+ # ok safe to assume this is in use
266
+ except Exception as e:
267
+ # logger.exception("whoops ", e)
268
+ pass
269
+
270
+ def cleanup_unused_files(self):
271
+ p = pathlib.Path(self.data_path)
272
+ items = list(p.glob("*undetected*"))
273
+ for item in items:
274
+ try:
275
+ item.unlink()
276
+ except:
277
+ pass
278
+
279
+ def patch(self):
280
+ self.patch_exe()
281
+ return self.is_binary_patched()
282
+
283
+ def fetch_release_number(self):
284
+ """
285
+ Gets the latest major version available, or the latest major version of self.target_version if set explicitly.
286
+ :return: version string
287
+ :rtype: LooseVersion
288
+ """
289
+ # Endpoint for old versions of Chromedriver (114 and below)
290
+ if self.is_old_chromedriver:
291
+ path = f"/latest_release_{self.version_main}"
292
+ path = path.upper()
293
+ logger.debug("getting release number from %s" % path)
294
+ return LooseVersion(urlopen(self.url_repo + path).read().decode())
295
+
296
+ # Endpoint for new versions of Chromedriver (115+)
297
+ if not self.version_main:
298
+ # Fetch the latest version
299
+ path = "/last-known-good-versions-with-downloads.json"
300
+ logger.debug("getting release number from %s" % path)
301
+ with urlopen(self.url_repo + path) as conn:
302
+ response = conn.read().decode()
303
+
304
+ last_versions = json.loads(response)
305
+ return LooseVersion(last_versions["channels"]["Stable"]["version"])
306
+
307
+ # Fetch the latest minor version of the major version provided
308
+ path = "/latest-versions-per-milestone-with-downloads.json"
309
+ logger.debug("getting release number from %s" % path)
310
+ with urlopen(self.url_repo + path) as conn:
311
+ response = conn.read().decode()
312
+
313
+ major_versions = json.loads(response)
314
+ return LooseVersion(major_versions["milestones"][str(self.version_main)]["version"])
315
+
316
+ def parse_exe_version(self):
317
+ with io.open(self.executable_path, "rb") as f:
318
+ for line in iter(lambda: f.readline(), b""):
319
+ match = re.search(rb"platform_handle\x00content\x00([0-9.]*)", line)
320
+ if match:
321
+ return LooseVersion(match[1].decode())
322
+
323
+ def fetch_package(self):
324
+ """
325
+ Downloads ChromeDriver from source
326
+
327
+ :return: path to downloaded file
328
+ """
329
+ zip_name = f"chromedriver_{self.platform_name}.zip"
330
+ if self.is_old_chromedriver:
331
+ download_url = "%s/%s/%s" % (self.url_repo, str(self.version_full), zip_name)
332
+ else:
333
+ zip_name = zip_name.replace("_", "-", 1)
334
+ download_url = "https://storage.googleapis.com/chrome-for-testing-public/%s/%s/%s"
335
+ download_url %= (str(self.version_full), self.platform_name, zip_name)
336
+
337
+ logger.debug("downloading from %s" % download_url)
338
+ return urlretrieve(download_url)[0]
339
+
340
+ def unzip_package(self, fp):
341
+ """
342
+ Does what it says
343
+
344
+ :return: path to unpacked executable
345
+ """
346
+ exe_path = self.exe_name
347
+ if not self.is_old_chromedriver:
348
+ # The new chromedriver unzips into its own folder
349
+ zip_name = f"chromedriver-{self.platform_name}"
350
+ exe_path = os.path.join(zip_name, self.exe_name)
351
+
352
+ logger.debug("unzipping %s" % fp)
353
+ try:
354
+ os.unlink(self.zip_path)
355
+ except (FileNotFoundError, OSError):
356
+ pass
357
+
358
+ os.makedirs(self.zip_path, mode=0o755, exist_ok=True)
359
+ with zipfile.ZipFile(fp, mode="r") as zf:
360
+ zf.extractall(self.zip_path)
361
+ os.rename(os.path.join(self.zip_path, exe_path), self.executable_path)
362
+ os.remove(fp)
363
+ shutil.rmtree
364
+ os.chmod(self.executable_path, 0o755)
365
+ return self.executable_path
366
+
367
+ @staticmethod
368
+ def force_kill_instances(exe_name):
369
+ """
370
+ kills running instances.
371
+ :param: executable name to kill, may be a path as well
372
+
373
+ :return: True on success else False
374
+ """
375
+ exe_name = os.path.basename(exe_name)
376
+ if IS_POSIX:
377
+ # Using shell=True for pidof, consider a more robust pid finding method if issues arise.
378
+ # pgrep can be an alternative: ["pgrep", "-f", exe_name]
379
+ # Or psutil if adding a dependency is acceptable.
380
+ command = f"pidof {exe_name}"
381
+ try:
382
+ result = subprocess.run(command, shell=True, capture_output=True, text=True, check=True)
383
+ pids = result.stdout.strip().split()
384
+ if pids:
385
+ subprocess.run(["kill", "-9"] + pids, check=False) # Changed from -f -9 to -9 as -f is not standard for kill
386
+ return True
387
+ return False # No PIDs found
388
+ except subprocess.CalledProcessError: # pidof returns 1 if no process found
389
+ return False # No process found
390
+ except Exception as e:
391
+ logger.debug(f"Error killing process on POSIX: {e}")
392
+ return False
393
+ else:
394
+ try:
395
+ # TASKKILL /F /IM chromedriver.exe
396
+ result = subprocess.run(["taskkill", "/f", "/im", exe_name], check=False, capture_output=True)
397
+ # taskkill returns 0 if process was killed, 128 if not found.
398
+ return result.returncode == 0
399
+ except Exception as e:
400
+ logger.debug(f"Error killing process on Windows: {e}")
401
+ return False
402
+
403
+ @staticmethod
404
+ def gen_random_cdc():
405
+ cdc = random.choices(string.ascii_letters, k=27)
406
+ return "".join(cdc).encode()
407
+
408
+ def is_binary_patched(self, executable_path=None):
409
+ executable_path = executable_path or self.executable_path
410
+ try:
411
+ with io.open(executable_path, "rb") as fh:
412
+ return fh.read().find(b"undetected chromedriver") != -1
413
+ except FileNotFoundError:
414
+ return False
415
+
416
+ def patch_exe(self):
417
+ start = time.perf_counter()
418
+ logger.info("patching driver executable %s" % self.executable_path)
419
+ with io.open(self.executable_path, "r+b") as fh:
420
+ content = fh.read()
421
+ # match_injected_codeblock = re.search(rb"{window.*;}", content)
422
+ match_injected_codeblock = re.search(rb"\{window\.cdc.*?;\}", content)
423
+ if match_injected_codeblock:
424
+ target_bytes = match_injected_codeblock[0]
425
+ new_target_bytes = (
426
+ b'{console.log("undetected chromedriver 1337!")}'.ljust(
427
+ len(target_bytes), b" "
428
+ )
429
+ )
430
+ new_content = content.replace(target_bytes, new_target_bytes)
431
+ if new_content == content:
432
+ logger.warning(
433
+ "something went wrong patching the driver binary. could not find injection code block"
434
+ )
435
+ else:
436
+ logger.debug(
437
+ "found block:\n%s\nreplacing with:\n%s"
438
+ % (target_bytes, new_target_bytes)
439
+ )
440
+ fh.seek(0)
441
+ fh.write(new_content)
442
+ logger.debug(
443
+ "patching took us {:.2f} seconds".format(time.perf_counter() - start)
444
+ )
445
+
446
+ def __repr__(self):
447
+ return "{0:s}({1:s})".format(
448
+ self.__class__.__name__,
449
+ self.executable_path,
450
+ )
451
+
452
+ def __del__(self):
453
+ if self._custom_exe_path:
454
+ # if the driver binary is specified by user
455
+ # we assume it is important enough to not delete it
456
+ return
457
+ else:
458
+ timeout = 3 # stop trying after this many seconds
459
+ t = time.monotonic()
460
+ now = lambda: time.monotonic()
461
+ while now() - t > timeout:
462
+ # we don't want to wait until the end of time
463
+ try:
464
+ if self.user_multi_procs:
465
+ break
466
+ os.unlink(self.executable_path)
467
+ logger.debug("successfully unlinked %s" % self.executable_path)
468
+ break
469
+ except (OSError, RuntimeError, PermissionError):
470
+ time.sleep(0.01)
471
+ continue
472
+ except FileNotFoundError:
473
+ break
src/undetected_chromedriver/reactor.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # this module is part of undetected_chromedriver
3
+
4
+ import asyncio
5
+ import json
6
+ import logging
7
+ import threading
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class Reactor(threading.Thread):
14
+ def __init__(self, driver: "Chrome"):
15
+ super().__init__()
16
+
17
+ self.driver = driver
18
+ self.loop = asyncio.new_event_loop()
19
+
20
+ self.lock = threading.Lock()
21
+ self.event = threading.Event()
22
+ self.daemon = True
23
+ self.handlers = {}
24
+
25
+ def add_event_handler(self, method_name, callback: callable):
26
+ """
27
+
28
+ Parameters
29
+ ----------
30
+ event_name: str
31
+ example "Network.responseReceived"
32
+
33
+ callback: callable
34
+ callable which accepts 1 parameter: the message object dictionary
35
+
36
+ Returns
37
+ -------
38
+
39
+ """
40
+ with self.lock:
41
+ self.handlers[method_name.lower()] = callback
42
+
43
+ @property
44
+ def running(self):
45
+ return not self.event.is_set()
46
+
47
+ def run(self):
48
+ try:
49
+ asyncio.set_event_loop(self.loop)
50
+ self.loop.run_until_complete(self.listen())
51
+ except Exception as e:
52
+ logger.warning("Reactor.run() => %s", e)
53
+
54
+ async def _wait_service_started(self):
55
+ while True:
56
+ with self.lock:
57
+ if (
58
+ getattr(self.driver, "service", None)
59
+ and getattr(self.driver.service, "process", None)
60
+ and self.driver.service.process.poll()
61
+ ):
62
+ await asyncio.sleep(self.driver._delay or 0.25)
63
+ else:
64
+ break
65
+
66
+ async def listen(self):
67
+ while self.running:
68
+ await self._wait_service_started()
69
+ await asyncio.sleep(1)
70
+
71
+ try:
72
+ with self.lock:
73
+ log_entries = self.driver.get_log("performance")
74
+
75
+ for entry in log_entries:
76
+ try:
77
+ obj_serialized: str = entry.get("message")
78
+ obj = json.loads(obj_serialized)
79
+ message = obj.get("message")
80
+ method = message.get("method")
81
+
82
+ if "*" in self.handlers:
83
+ await self.loop.run_in_executor(
84
+ None, self.handlers["*"], message
85
+ )
86
+ elif method.lower() in self.handlers:
87
+ await self.loop.run_in_executor(
88
+ None, self.handlers[method.lower()], message
89
+ )
90
+
91
+ # print(type(message), message)
92
+ except Exception as e:
93
+ raise e from None
94
+
95
+ except Exception as e:
96
+ if "invalid session id" in str(e):
97
+ pass
98
+ else:
99
+ logging.debug("exception ignored :", e)
src/undetected_chromedriver/webelement.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+
3
+ from selenium.webdriver.common.by import By
4
+ import selenium.webdriver.remote.webelement
5
+
6
+
7
+ class WebElement(selenium.webdriver.remote.webelement.WebElement):
8
+ def click_safe(self):
9
+ super().click()
10
+ self._parent.reconnect(0.1)
11
+
12
+ def children(
13
+ self, tag=None, recursive=False
14
+ ) -> List[selenium.webdriver.remote.webelement.WebElement]:
15
+ """
16
+ returns direct child elements of current element
17
+ :param tag: str, if supplied, returns <tag> nodes only
18
+ """
19
+ script = "return [... arguments[0].children]"
20
+ if tag:
21
+ script += ".filter( node => node.tagName === '%s')" % tag.upper()
22
+ if recursive:
23
+ return list(_recursive_children(self, tag))
24
+ return list(self._parent.execute_script(script, self))
25
+
26
+
27
+ class UCWebElement(WebElement):
28
+ """
29
+ Custom WebElement class which makes it easier to view elements when
30
+ working in an interactive environment.
31
+
32
+ standard webelement repr:
33
+ <selenium.webdriver.remote.webelement.WebElement (session="85ff0f671512fa535630e71ee951b1f2", element="6357cb55-92c3-4c0f-9416-b174f9c1b8c4")>
34
+
35
+ using this WebElement class:
36
+ <WebElement(<a class="mobile-show-inline-block mc-update-infos init-ok" href="#" id="main-cat-switcher-mobile">)>
37
+
38
+ """
39
+
40
+ def __init__(self, parent, id_):
41
+ super().__init__(parent, id_)
42
+ self._attrs = None
43
+
44
+ @property
45
+ def attrs(self):
46
+ if not self._attrs:
47
+ self._attrs = self._parent.execute_script(
48
+ """
49
+ var items = {};
50
+ for (index = 0; index < arguments[0].attributes.length; ++index)
51
+ {
52
+ items[arguments[0].attributes[index].name] = arguments[0].attributes[index].value
53
+ };
54
+ return items;
55
+ """,
56
+ self,
57
+ )
58
+ return self._attrs
59
+
60
+ def __repr__(self):
61
+ strattrs = " ".join([f'{k}="{v}"' for k, v in self.attrs.items()])
62
+ if strattrs:
63
+ strattrs = " " + strattrs
64
+ return f"{self.__class__.__name__} <{self.tag_name}{strattrs}>"
65
+
66
+
67
+ def _recursive_children(element, tag: str = None, _results=None):
68
+ """
69
+ returns all children of <element> recursively
70
+
71
+ :param element: `WebElement` object.
72
+ find children below this <element>
73
+
74
+ :param tag: str = None.
75
+ if provided, return only <tag> elements. example: 'a', or 'img'
76
+ :param _results: do not use!
77
+ """
78
+ results = _results or set()
79
+ for element in element.children():
80
+ if tag:
81
+ if element.tag_name == tag:
82
+ results.add(element)
83
+ else:
84
+ results.add(element)
85
+ results |= _recursive_children(element, tag, results)
86
+ return results
src/utils.py ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import os
4
+ import platform
5
+ import re
6
+ import shutil
7
+ import sys
8
+ import tempfile
9
+ import urllib.parse
10
+
11
+ from selenium.webdriver.chrome.webdriver import WebDriver
12
+ import undetected_chromedriver as uc
13
+
14
+ FLARESOLVERR_VERSION = None
15
+ PLATFORM_VERSION = None
16
+ CHROME_EXE_PATH = None
17
+ CHROME_MAJOR_VERSION = None
18
+ USER_AGENT = None
19
+ XVFB_DISPLAY = None
20
+ PATCHED_DRIVER_PATH = None
21
+
22
+
23
+ def get_config_log_html() -> bool:
24
+ return os.environ.get('LOG_HTML', 'false').lower() == 'true'
25
+
26
+
27
+ def get_config_headless() -> bool:
28
+ return os.environ.get('HEADLESS', 'true').lower() == 'true'
29
+
30
+
31
+ def get_config_disable_media() -> bool:
32
+ return os.environ.get('DISABLE_MEDIA', 'false').lower() == 'true'
33
+
34
+
35
+ def get_flaresolverr_version() -> str:
36
+ global FLARESOLVERR_VERSION
37
+ if FLARESOLVERR_VERSION is not None:
38
+ return FLARESOLVERR_VERSION
39
+
40
+ package_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, 'package.json')
41
+ if not os.path.isfile(package_path):
42
+ package_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'package.json')
43
+ with open(package_path) as f:
44
+ FLARESOLVERR_VERSION = json.loads(f.read())['version']
45
+ return FLARESOLVERR_VERSION
46
+
47
+ def get_current_platform() -> str:
48
+ global PLATFORM_VERSION
49
+ if PLATFORM_VERSION is not None:
50
+ return PLATFORM_VERSION
51
+ PLATFORM_VERSION = os.name
52
+ return PLATFORM_VERSION
53
+
54
+
55
+ def create_proxy_extension(proxy: dict) -> str:
56
+ parsed_url = urllib.parse.urlparse(proxy['url'])
57
+ scheme = parsed_url.scheme
58
+ host = parsed_url.hostname
59
+ port = parsed_url.port
60
+ username = proxy['username']
61
+ password = proxy['password']
62
+ manifest_json = """
63
+ {
64
+ "version": "1.0.0",
65
+ "manifest_version": 3,
66
+ "name": "Chrome Proxy",
67
+ "permissions": [
68
+ "proxy",
69
+ "tabs",
70
+ "storage",
71
+ "webRequest",
72
+ "webRequestAuthProvider"
73
+ ],
74
+ "host_permissions": [
75
+ "<all_urls>"
76
+ ],
77
+ "background": {
78
+ "service_worker": "background.js"
79
+ },
80
+ "minimum_chrome_version": "76.0.0"
81
+ }
82
+ """
83
+
84
+ background_js = """
85
+ var config = {
86
+ mode: "fixed_servers",
87
+ rules: {
88
+ singleProxy: {
89
+ scheme: "%s",
90
+ host: "%s",
91
+ port: %d
92
+ },
93
+ bypassList: ["localhost"]
94
+ }
95
+ };
96
+
97
+ chrome.proxy.settings.set({value: config, scope: "regular"}, function() {});
98
+
99
+ function callbackFn(details) {
100
+ return {
101
+ authCredentials: {
102
+ username: "%s",
103
+ password: "%s"
104
+ }
105
+ };
106
+ }
107
+
108
+ chrome.webRequest.onAuthRequired.addListener(
109
+ callbackFn,
110
+ { urls: ["<all_urls>"] },
111
+ ['blocking']
112
+ );
113
+ """ % (
114
+ scheme,
115
+ host,
116
+ port,
117
+ username,
118
+ password
119
+ )
120
+
121
+ proxy_extension_dir = tempfile.mkdtemp()
122
+
123
+ with open(os.path.join(proxy_extension_dir, "manifest.json"), "w") as f:
124
+ f.write(manifest_json)
125
+
126
+ with open(os.path.join(proxy_extension_dir, "background.js"), "w") as f:
127
+ f.write(background_js)
128
+
129
+ return proxy_extension_dir
130
+
131
+
132
+ def get_webdriver(proxy: dict = None) -> WebDriver:
133
+ global PATCHED_DRIVER_PATH, USER_AGENT
134
+ logging.debug('Launching web browser...')
135
+
136
+ # undetected_chromedriver
137
+ options = uc.ChromeOptions()
138
+ options.add_argument('--no-sandbox')
139
+ options.add_argument('--window-size=1920,1080')
140
+ options.add_argument('--disable-search-engine-choice-screen')
141
+ # todo: this param shows a warning in chrome head-full
142
+ options.add_argument('--disable-setuid-sandbox')
143
+ options.add_argument('--disable-dev-shm-usage')
144
+ # this option removes the zygote sandbox (it seems that the resolution is a bit faster)
145
+ options.add_argument('--no-zygote')
146
+ # attempt to fix Docker ARM32 build
147
+ IS_ARMARCH = platform.machine().startswith(('arm', 'aarch'))
148
+ if IS_ARMARCH:
149
+ options.add_argument('--disable-gpu-sandbox')
150
+ options.add_argument('--ignore-certificate-errors')
151
+ options.add_argument('--ignore-ssl-errors')
152
+
153
+ language = os.environ.get('LANG', None)
154
+ if language is not None:
155
+ options.add_argument('--accept-lang=%s' % language)
156
+
157
+ # Fix for Chrome 117 | https://github.com/FlareSolverr/FlareSolverr/issues/910
158
+ if USER_AGENT is not None:
159
+ options.add_argument('--user-agent=%s' % USER_AGENT)
160
+
161
+ proxy_extension_dir = None
162
+ if proxy and all(key in proxy for key in ['url', 'username', 'password']):
163
+ proxy_extension_dir = create_proxy_extension(proxy)
164
+ options.add_argument("--disable-features=DisableLoadExtensionCommandLineSwitch")
165
+ options.add_argument("--load-extension=%s" % os.path.abspath(proxy_extension_dir))
166
+ elif proxy and 'url' in proxy:
167
+ proxy_url = proxy['url']
168
+ logging.debug("Using webdriver proxy: %s", proxy_url)
169
+ options.add_argument('--proxy-server=%s' % proxy_url)
170
+
171
+ # note: headless mode is detected (headless = True)
172
+ # we launch the browser in head-full mode with the window hidden
173
+ windows_headless = False
174
+ if get_config_headless():
175
+ if os.name == 'nt':
176
+ windows_headless = True
177
+ else:
178
+ start_xvfb_display()
179
+ # For normal headless mode:
180
+ # options.add_argument('--headless')
181
+
182
+ # if we are inside the Docker container, we avoid downloading the driver
183
+ driver_exe_path = None
184
+ version_main = None
185
+ if os.path.exists("/app/chromedriver"):
186
+ # running inside Docker
187
+ driver_exe_path = "/app/chromedriver"
188
+ else:
189
+ version_main = get_chrome_major_version()
190
+ if PATCHED_DRIVER_PATH is not None:
191
+ driver_exe_path = PATCHED_DRIVER_PATH
192
+
193
+ # detect chrome path
194
+ browser_executable_path = get_chrome_exe_path()
195
+
196
+ # downloads and patches the chromedriver
197
+ # if we don't set driver_executable_path it downloads, patches, and deletes the driver each time
198
+ try:
199
+ driver = uc.Chrome(options=options, browser_executable_path=browser_executable_path,
200
+ driver_executable_path=driver_exe_path, version_main=version_main,
201
+ windows_headless=windows_headless, headless=get_config_headless())
202
+ except Exception as e:
203
+ logging.error("Error starting Chrome: %s" % e)
204
+ # No point in continuing if we cannot retrieve the driver
205
+ raise e
206
+
207
+ # save the patched driver to avoid re-downloads
208
+ if driver_exe_path is None:
209
+ PATCHED_DRIVER_PATH = os.path.join(driver.patcher.data_path, driver.patcher.exe_name)
210
+ if PATCHED_DRIVER_PATH != driver.patcher.executable_path:
211
+ shutil.copy(driver.patcher.executable_path, PATCHED_DRIVER_PATH)
212
+
213
+ # clean up proxy extension directory
214
+ if proxy_extension_dir is not None:
215
+ shutil.rmtree(proxy_extension_dir)
216
+
217
+ # selenium vanilla
218
+ # options = webdriver.ChromeOptions()
219
+ # options.add_argument('--no-sandbox')
220
+ # options.add_argument('--window-size=1920,1080')
221
+ # options.add_argument('--disable-setuid-sandbox')
222
+ # options.add_argument('--disable-dev-shm-usage')
223
+ # driver = webdriver.Chrome(options=options)
224
+
225
+ return driver
226
+
227
+
228
+ def get_chrome_exe_path() -> str:
229
+ global CHROME_EXE_PATH
230
+ if CHROME_EXE_PATH is not None:
231
+ return CHROME_EXE_PATH
232
+ # linux pyinstaller bundle
233
+ chrome_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chrome', "chrome")
234
+ if os.path.exists(chrome_path):
235
+ if not os.access(chrome_path, os.X_OK):
236
+ raise Exception(f'Chrome binary "{chrome_path}" is not executable. '
237
+ f'Please, extract the archive with "tar xzf <file.tar.gz>".')
238
+ CHROME_EXE_PATH = chrome_path
239
+ return CHROME_EXE_PATH
240
+ # windows pyinstaller bundle
241
+ chrome_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'chrome', "chrome.exe")
242
+ if os.path.exists(chrome_path):
243
+ CHROME_EXE_PATH = chrome_path
244
+ return CHROME_EXE_PATH
245
+ # system
246
+ CHROME_EXE_PATH = uc.find_chrome_executable()
247
+ return CHROME_EXE_PATH
248
+
249
+
250
+ def get_chrome_major_version() -> str:
251
+ global CHROME_MAJOR_VERSION
252
+ if CHROME_MAJOR_VERSION is not None:
253
+ return CHROME_MAJOR_VERSION
254
+
255
+ if os.name == 'nt':
256
+ # Example: '104.0.5112.79'
257
+ try:
258
+ complete_version = extract_version_nt_executable(get_chrome_exe_path())
259
+ except Exception:
260
+ try:
261
+ complete_version = extract_version_nt_registry()
262
+ except Exception:
263
+ # Example: '104.0.5112.79'
264
+ complete_version = extract_version_nt_folder()
265
+ else:
266
+ chrome_path = get_chrome_exe_path()
267
+ process = os.popen(f'"{chrome_path}" --version')
268
+ # Example 1: 'Chromium 104.0.5112.79 Arch Linux\n'
269
+ # Example 2: 'Google Chrome 104.0.5112.79 Arch Linux\n'
270
+ complete_version = process.read()
271
+ process.close()
272
+
273
+ CHROME_MAJOR_VERSION = complete_version.split('.')[0].split(' ')[-1]
274
+ return CHROME_MAJOR_VERSION
275
+
276
+
277
+ def extract_version_nt_executable(exe_path: str) -> str:
278
+ import pefile
279
+ pe = pefile.PE(exe_path, fast_load=True)
280
+ pe.parse_data_directories(
281
+ directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_RESOURCE"]]
282
+ )
283
+ return pe.FileInfo[0][0].StringTable[0].entries[b"FileVersion"].decode('utf-8')
284
+
285
+
286
+ def extract_version_nt_registry() -> str:
287
+ stream = os.popen(
288
+ 'reg query "HKLM\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Google Chrome"')
289
+ output = stream.read()
290
+ google_version = ''
291
+ for letter in output[output.rindex('DisplayVersion REG_SZ') + 24:]:
292
+ if letter != '\n':
293
+ google_version += letter
294
+ else:
295
+ break
296
+ return google_version.strip()
297
+
298
+
299
+ def extract_version_nt_folder() -> str:
300
+ # Check if the Chrome folder exists in the x32 or x64 Program Files folders.
301
+ for i in range(2):
302
+ path = 'C:\\Program Files' + (' (x86)' if i else '') + '\\Google\\Chrome\\Application'
303
+ if os.path.isdir(path):
304
+ paths = [f.path for f in os.scandir(path) if f.is_dir()]
305
+ for path in paths:
306
+ filename = os.path.basename(path)
307
+ pattern = r'\d+\.\d+\.\d+\.\d+'
308
+ match = re.search(pattern, filename)
309
+ if match and match.group():
310
+ # Found a Chrome version.
311
+ return match.group(0)
312
+ return ''
313
+
314
+
315
+ def get_user_agent(driver=None) -> str:
316
+ global USER_AGENT
317
+ if USER_AGENT is not None:
318
+ return USER_AGENT
319
+
320
+ try:
321
+ if driver is None:
322
+ driver = get_webdriver()
323
+ USER_AGENT = driver.execute_script("return navigator.userAgent")
324
+ # Fix for Chrome 117 | https://github.com/FlareSolverr/FlareSolverr/issues/910
325
+ USER_AGENT = re.sub('HEADLESS', '', USER_AGENT, flags=re.IGNORECASE)
326
+ return USER_AGENT
327
+ except Exception as e:
328
+ raise Exception("Error getting browser User-Agent. " + str(e))
329
+ finally:
330
+ if driver is not None:
331
+ if PLATFORM_VERSION == "nt":
332
+ driver.close()
333
+ driver.quit()
334
+
335
+
336
+ def start_xvfb_display():
337
+ global XVFB_DISPLAY
338
+ if XVFB_DISPLAY is None:
339
+ from xvfbwrapper import Xvfb
340
+ XVFB_DISPLAY = Xvfb()
341
+ XVFB_DISPLAY.start()
342
+
343
+
344
+ def object_to_dict(_object):
345
+ json_dict = json.loads(json.dumps(_object, default=lambda o: o.__dict__))
346
+ # remove hidden fields
347
+ return {k: v for k, v in json_dict.items() if not k.startswith('__')}
test-requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ WebTest==3.0.7