zyztitan commited on
Commit
d1ffb6d
·
verified ·
1 Parent(s): a876d8d

Upload 33 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.13-slim-bookworm AS builder
2
+
3
+ # Build dummy packages to skip installing them and their dependencies
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 for libgl1-mesa-dri\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 for adwaita-icon-theme\n' >> adwaita-icon-theme \
12
+ && equivs-build adwaita-icon-theme \
13
+ && mv adwaita-icon-theme_*.deb /adwaita-icon-theme.deb
14
+
15
+ FROM python:3.13-slim-bookworm
16
+
17
+ # Copy dummy packages
18
+ COPY --from=builder /*.deb /
19
+
20
+ # Install dependencies and create flaresolverr user
21
+ # You can test Chromium running this command inside the container:
22
+ # xvfb-run -s "-screen 0 1600x1200x24" chromium --no-sandbox
23
+ # The error traces is like this: "*** stack smashing detected ***: terminated"
24
+ # To check the package versions available you can use this command:
25
+ # apt-cache madison chromium
26
+ WORKDIR /app
27
+ # Install dummy packages
28
+ RUN dpkg -i /libgl1-mesa-dri.deb \
29
+ && dpkg -i /adwaita-icon-theme.deb \
30
+ # Install dependencies
31
+ && apt-get update \
32
+ && apt-get install -y --no-install-recommends chromium chromium-common chromium-driver xvfb dumb-init \
33
+ procps curl vim xauth \
34
+ # Remove temporary files and hardware decoding libraries
35
+ && rm -rf /var/lib/apt/lists/* \
36
+ && rm -f /usr/lib/x86_64-linux-gnu/libmfxhw* \
37
+ && rm -f /usr/lib/x86_64-linux-gnu/mfx/* \
38
+ # Create flaresolverr user
39
+ && useradd --home-dir /app --shell /bin/sh flaresolverr \
40
+ && mv /usr/bin/chromedriver chromedriver \
41
+ && chown -R flaresolverr:flaresolverr . \
42
+ # Create config dir
43
+ && mkdir /config \
44
+ && chown flaresolverr:flaresolverr /config
45
+
46
+ VOLUME /config
47
+
48
+ # Install Python dependencies
49
+ COPY requirements.txt .
50
+ RUN pip install -r requirements.txt \
51
+ # Remove temporary files
52
+ && rm -rf /root/.cache
53
+
54
+ USER flaresolverr
55
+
56
+ RUN mkdir -p "/app/.config/chromium/Crash Reports/pending"
57
+
58
+ COPY src .
59
+ COPY package.json ../
60
+
61
+ EXPOSE 8191
62
+ EXPOSE 8192
63
+
64
+ # dumb-init avoids zombie chromium processes
65
+ ENTRYPOINT ["/usr/bin/dumb-init", "--"]
66
+
67
+ CMD ["/usr/local/bin/python", "-u", "/app/flaresolverr.py"]
68
+
69
+ # Local build
70
+ # docker build -t ngosang/flaresolverr:3.4.6 .
71
+ # docker run -p 8191:8191 ngosang/flaresolverr:3.4.6
72
+
73
+ # Multi-arch build
74
+ # docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
75
+ # docker buildx create --use
76
+ # docker buildx build -t ngosang/flaresolverr:3.4.6 --platform linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8 .
77
+ # add --push to publish in DockerHub
78
+
79
+ # Test multi-arch build
80
+ # docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
81
+ # docker buildx create --use
82
+ # docker buildx build -t ngosang/flaresolverr:3.4.6 --platform linux/arm/v7 --load .
83
+ # docker run -p 8191:8191 --platform linux/arm/v7 ngosang/flaresolverr:3.4.6
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.
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('__')}