funbrowser 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- funbrowser/__init__.py +120 -0
- funbrowser/_cdp.py +181 -0
- funbrowser/_errors.py +32 -0
- funbrowser/_flags.py +89 -0
- funbrowser/_launcher.py +153 -0
- funbrowser/browser.py +281 -0
- funbrowser/context.py +163 -0
- funbrowser/context_pool.py +162 -0
- funbrowser/element.py +258 -0
- funbrowser/fingerprint/__init__.py +14 -0
- funbrowser/fingerprint/data.py +74 -0
- funbrowser/fingerprint/presets.py +588 -0
- funbrowser/geo.py +139 -0
- funbrowser/humanly.py +188 -0
- funbrowser/panel.py +1181 -0
- funbrowser/pool.py +152 -0
- funbrowser/profile.py +73 -0
- funbrowser/proxy.py +236 -0
- funbrowser/py.typed +0 -0
- funbrowser/solver/__init__.py +12 -0
- funbrowser/solver/bridge.py +167 -0
- funbrowser/solver/client.py +244 -0
- funbrowser/solver/scripts/__init__.py +0 -0
- funbrowser/solver/scripts/_bootstrap.js +30 -0
- funbrowser/solver/scripts/funcaptcha.js +74 -0
- funbrowser/solver/scripts/geetest.js +76 -0
- funbrowser/solver/scripts/hcaptcha.js +76 -0
- funbrowser/solver/scripts/recaptcha_v2.js +79 -0
- funbrowser/solver/scripts/recaptcha_v3.js +45 -0
- funbrowser/solver/scripts/turnstile.js +60 -0
- funbrowser/stealth/__init__.py +13 -0
- funbrowser/stealth/flags.py +54 -0
- funbrowser/stealth/patches.py +214 -0
- funbrowser/stealth/scripts/__init__.py +0 -0
- funbrowser/stealth/scripts/_camouflage.js +32 -0
- funbrowser/stealth/scripts/_cleanup.js +8 -0
- funbrowser/stealth/scripts/audio_noise.js +32 -0
- funbrowser/stealth/scripts/canvas_noise.js +43 -0
- funbrowser/stealth/scripts/chrome_runtime.js +53 -0
- funbrowser/stealth/scripts/hardware.js +15 -0
- funbrowser/stealth/scripts/languages.js +13 -0
- funbrowser/stealth/scripts/permissions.js +15 -0
- funbrowser/stealth/scripts/platform.js +18 -0
- funbrowser/stealth/scripts/plugins.js +37 -0
- funbrowser/stealth/scripts/screen_props.js +18 -0
- funbrowser/stealth/scripts/webdriver.js +14 -0
- funbrowser/stealth/scripts/webgl.js +27 -0
- funbrowser/stealth/scripts/webrtc.js +45 -0
- funbrowser/tab.py +345 -0
- funbrowser/tls/__init__.py +25 -0
- funbrowser/tls/ca.py +181 -0
- funbrowser/tls/http.py +145 -0
- funbrowser/tls/mitm.py +326 -0
- funbrowser-0.1.0.dist-info/METADATA +316 -0
- funbrowser-0.1.0.dist-info/RECORD +57 -0
- funbrowser-0.1.0.dist-info/WHEEL +4 -0
- funbrowser-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Async client for the funsolver.com captcha API.
|
|
2
|
+
|
|
3
|
+
The API surface follows the 2captcha / anti-captcha JSON convention
|
|
4
|
+
(``POST /createTask`` then poll ``POST /getTaskResult``). If the actual
|
|
5
|
+
funsolver.com endpoints diverge, override ``base_url`` or subclass and
|
|
6
|
+
override ``_create_task`` / ``_poll`` rather than monkey-patching.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FunSolverError(Exception):
|
|
21
|
+
"""A funsolver.com request failed or returned an error response."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str, *, code: int | None = None) -> None:
|
|
24
|
+
super().__init__(message)
|
|
25
|
+
self.code = code
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FunSolverClient:
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
api_key: str,
|
|
32
|
+
*,
|
|
33
|
+
base_url: str = "https://api.funsolver.com",
|
|
34
|
+
timeout: float = 120.0,
|
|
35
|
+
poll_interval: float = 3.0,
|
|
36
|
+
http_timeout: float = 30.0,
|
|
37
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
self._api_key = api_key
|
|
40
|
+
self._base = base_url.rstrip("/")
|
|
41
|
+
self._timeout = timeout
|
|
42
|
+
self._poll_interval = poll_interval
|
|
43
|
+
self._http = httpx.AsyncClient(timeout=http_timeout, transport=transport)
|
|
44
|
+
|
|
45
|
+
async def close(self) -> None:
|
|
46
|
+
await self._http.aclose()
|
|
47
|
+
|
|
48
|
+
async def __aenter__(self) -> FunSolverClient:
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
52
|
+
await self.close()
|
|
53
|
+
|
|
54
|
+
async def balance(self) -> float:
|
|
55
|
+
data = await self._post("/getBalance", {"clientKey": self._api_key})
|
|
56
|
+
return float(data.get("balance", 0.0))
|
|
57
|
+
|
|
58
|
+
async def solve_turnstile(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
sitekey: str,
|
|
62
|
+
page_url: str,
|
|
63
|
+
action: str | None = None,
|
|
64
|
+
cdata: str | None = None,
|
|
65
|
+
) -> str:
|
|
66
|
+
task: dict[str, Any] = {
|
|
67
|
+
"type": "TurnstileTask",
|
|
68
|
+
"websiteURL": page_url,
|
|
69
|
+
"websiteKey": sitekey,
|
|
70
|
+
}
|
|
71
|
+
if action is not None:
|
|
72
|
+
task["action"] = action
|
|
73
|
+
if cdata is not None:
|
|
74
|
+
task["data"] = cdata
|
|
75
|
+
return await self._solve(task)
|
|
76
|
+
|
|
77
|
+
async def solve_recaptcha_v2(
|
|
78
|
+
self,
|
|
79
|
+
*,
|
|
80
|
+
sitekey: str,
|
|
81
|
+
page_url: str,
|
|
82
|
+
invisible: bool = False,
|
|
83
|
+
data_s: str | None = None,
|
|
84
|
+
is_enterprise: bool = False,
|
|
85
|
+
) -> str:
|
|
86
|
+
"""reCAPTCHA v2 — both checkbox and invisible variants.
|
|
87
|
+
|
|
88
|
+
``data_s`` is the value of the ``data-s`` attribute used on
|
|
89
|
+
Google-owned properties; safe to omit elsewhere. ``is_enterprise``
|
|
90
|
+
switches the task type for Enterprise sites.
|
|
91
|
+
"""
|
|
92
|
+
if is_enterprise:
|
|
93
|
+
task_type = "RecaptchaV2EnterpriseTaskProxyless"
|
|
94
|
+
else:
|
|
95
|
+
task_type = "RecaptchaV2TaskProxyless"
|
|
96
|
+
task: dict[str, Any] = {
|
|
97
|
+
"type": task_type,
|
|
98
|
+
"websiteURL": page_url,
|
|
99
|
+
"websiteKey": sitekey,
|
|
100
|
+
}
|
|
101
|
+
if invisible:
|
|
102
|
+
task["isInvisible"] = True
|
|
103
|
+
if data_s is not None:
|
|
104
|
+
task["recaptchaDataSValue"] = data_s
|
|
105
|
+
return await self._solve(task)
|
|
106
|
+
|
|
107
|
+
async def solve_recaptcha_v3(
|
|
108
|
+
self,
|
|
109
|
+
*,
|
|
110
|
+
sitekey: str,
|
|
111
|
+
page_url: str,
|
|
112
|
+
action: str = "verify",
|
|
113
|
+
min_score: float = 0.7,
|
|
114
|
+
is_enterprise: bool = False,
|
|
115
|
+
) -> str:
|
|
116
|
+
"""reCAPTCHA v3 — score-based, needs the page's expected action name."""
|
|
117
|
+
task_type = (
|
|
118
|
+
"RecaptchaV3EnterpriseTaskProxyless" if is_enterprise else "RecaptchaV3TaskProxyless"
|
|
119
|
+
)
|
|
120
|
+
return await self._solve(
|
|
121
|
+
{
|
|
122
|
+
"type": task_type,
|
|
123
|
+
"websiteURL": page_url,
|
|
124
|
+
"websiteKey": sitekey,
|
|
125
|
+
"pageAction": action,
|
|
126
|
+
"minScore": min_score,
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async def solve_hcaptcha(
|
|
131
|
+
self,
|
|
132
|
+
*,
|
|
133
|
+
sitekey: str,
|
|
134
|
+
page_url: str,
|
|
135
|
+
is_invisible: bool = False,
|
|
136
|
+
enterprise_payload: dict[str, Any] | None = None,
|
|
137
|
+
) -> str:
|
|
138
|
+
task: dict[str, Any] = {
|
|
139
|
+
"type": "HCaptchaTaskProxyless",
|
|
140
|
+
"websiteURL": page_url,
|
|
141
|
+
"websiteKey": sitekey,
|
|
142
|
+
}
|
|
143
|
+
if is_invisible:
|
|
144
|
+
task["isInvisible"] = True
|
|
145
|
+
if enterprise_payload is not None:
|
|
146
|
+
task["enterprisePayload"] = enterprise_payload
|
|
147
|
+
return await self._solve(task)
|
|
148
|
+
|
|
149
|
+
async def solve_funcaptcha(
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
public_key: str,
|
|
153
|
+
page_url: str,
|
|
154
|
+
surl: str | None = None,
|
|
155
|
+
data: dict[str, Any] | None = None,
|
|
156
|
+
) -> str:
|
|
157
|
+
"""FunCaptcha / Arkose Labs.
|
|
158
|
+
|
|
159
|
+
``public_key`` is the value of the widget's ``data-pkey``.
|
|
160
|
+
``surl`` is the FunCaptcha API URL — required for some setups,
|
|
161
|
+
``data`` carries additional context the site computes (the
|
|
162
|
+
encrypted ``data`` blob).
|
|
163
|
+
"""
|
|
164
|
+
task: dict[str, Any] = {
|
|
165
|
+
"type": "FunCaptchaTaskProxyless",
|
|
166
|
+
"websiteURL": page_url,
|
|
167
|
+
"websitePublicKey": public_key,
|
|
168
|
+
}
|
|
169
|
+
if surl is not None:
|
|
170
|
+
task["funcaptchaApiJSSubdomain"] = surl
|
|
171
|
+
if data is not None:
|
|
172
|
+
task["data"] = data
|
|
173
|
+
return await self._solve(task)
|
|
174
|
+
|
|
175
|
+
async def solve_geetest(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
gt: str,
|
|
179
|
+
challenge: str,
|
|
180
|
+
page_url: str,
|
|
181
|
+
api_server: str | None = None,
|
|
182
|
+
version: int = 3,
|
|
183
|
+
init_parameters: dict[str, Any] | None = None,
|
|
184
|
+
) -> str:
|
|
185
|
+
"""GeeTest v3/v4.
|
|
186
|
+
|
|
187
|
+
For v3: pass ``gt`` and ``challenge`` (both fetched from the
|
|
188
|
+
site's challenge endpoint just before solving). For v4: pass
|
|
189
|
+
``gt`` as the captchaId and set ``version=4``.
|
|
190
|
+
"""
|
|
191
|
+
task: dict[str, Any] = {
|
|
192
|
+
"type": "GeeTestTaskProxyless",
|
|
193
|
+
"websiteURL": page_url,
|
|
194
|
+
"gt": gt,
|
|
195
|
+
"challenge": challenge,
|
|
196
|
+
"version": version,
|
|
197
|
+
}
|
|
198
|
+
if api_server is not None:
|
|
199
|
+
task["geetestApiServerSubdomain"] = api_server
|
|
200
|
+
if init_parameters is not None:
|
|
201
|
+
task["initParameters"] = init_parameters
|
|
202
|
+
return await self._solve(task)
|
|
203
|
+
|
|
204
|
+
async def _solve(self, task: dict[str, Any]) -> str:
|
|
205
|
+
task_id = await self._create_task(task)
|
|
206
|
+
loop = asyncio.get_running_loop()
|
|
207
|
+
deadline = loop.time() + self._timeout
|
|
208
|
+
while True:
|
|
209
|
+
await asyncio.sleep(self._poll_interval)
|
|
210
|
+
data = await self._post(
|
|
211
|
+
"/getTaskResult",
|
|
212
|
+
{"clientKey": self._api_key, "taskId": task_id},
|
|
213
|
+
)
|
|
214
|
+
status = data.get("status")
|
|
215
|
+
if status == "ready":
|
|
216
|
+
token = data.get("solution", {}).get("token")
|
|
217
|
+
if not token:
|
|
218
|
+
raise FunSolverError("solver returned ready without a token")
|
|
219
|
+
return str(token)
|
|
220
|
+
if loop.time() > deadline:
|
|
221
|
+
raise FunSolverError(f"task {task_id} did not complete within {self._timeout}s")
|
|
222
|
+
|
|
223
|
+
async def _create_task(self, task: dict[str, Any]) -> int:
|
|
224
|
+
data = await self._post(
|
|
225
|
+
"/createTask",
|
|
226
|
+
{"clientKey": self._api_key, "task": task},
|
|
227
|
+
)
|
|
228
|
+
task_id = data.get("taskId")
|
|
229
|
+
if not task_id:
|
|
230
|
+
raise FunSolverError("solver did not return a taskId")
|
|
231
|
+
return int(task_id)
|
|
232
|
+
|
|
233
|
+
async def _post(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
234
|
+
r = await self._http.post(f"{self._base}{path}", json=body)
|
|
235
|
+
r.raise_for_status()
|
|
236
|
+
data = r.json()
|
|
237
|
+
if not isinstance(data, dict):
|
|
238
|
+
raise FunSolverError(f"non-dict response from {path}: {data!r}")
|
|
239
|
+
if data.get("errorId"):
|
|
240
|
+
raise FunSolverError(
|
|
241
|
+
data.get("errorDescription") or data.get("errorCode") or "solver error",
|
|
242
|
+
code=int(data.get("errorId", 0)),
|
|
243
|
+
)
|
|
244
|
+
return data
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
if (window.__funbrowser) return;
|
|
3
|
+
const pending = new Map();
|
|
4
|
+
let nextId = 0;
|
|
5
|
+
|
|
6
|
+
window.__funbrowser = {
|
|
7
|
+
solve(req) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const id = ++nextId;
|
|
10
|
+
pending.set(id, { resolve, reject });
|
|
11
|
+
// The binding is a CDP-injected function that takes a single string.
|
|
12
|
+
try {
|
|
13
|
+
window.__funbrowser_solve(JSON.stringify({ id, ...req }));
|
|
14
|
+
} catch (e) {
|
|
15
|
+
pending.delete(id);
|
|
16
|
+
reject(e);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Called by the host (Python side) via Runtime.evaluate.
|
|
23
|
+
window.__funbrowser_resolve = (id, result) => {
|
|
24
|
+
const p = pending.get(id);
|
|
25
|
+
if (!p) return;
|
|
26
|
+
pending.delete(id);
|
|
27
|
+
if (result && result.ok) p.resolve(result.token);
|
|
28
|
+
else p.reject(new Error((result && result.error) || 'funsolver: unknown error'));
|
|
29
|
+
};
|
|
30
|
+
})();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// FunCaptcha / Arkose Labs.
|
|
2
|
+
//
|
|
3
|
+
// Common attach points:
|
|
4
|
+
// <div class="funcaptcha" data-pkey="..."></div>
|
|
5
|
+
// <input data-pkey="..."> (some sites mark it on an input)
|
|
6
|
+
// iframe[src*="funcaptcha"] → look up parent for data-pkey
|
|
7
|
+
//
|
|
8
|
+
// Response goes into one of: input[name="fc-token"],
|
|
9
|
+
// input[name="arkose-token"], input[name="verification-token"].
|
|
10
|
+
// Optional data-callback name on the host element.
|
|
11
|
+
(() => {
|
|
12
|
+
if (!window.__funbrowser) return;
|
|
13
|
+
if (window.__funbrowser._funcaptchaInstalled) return;
|
|
14
|
+
window.__funbrowser._funcaptchaInstalled = true;
|
|
15
|
+
|
|
16
|
+
const seen = new WeakSet();
|
|
17
|
+
|
|
18
|
+
function injectToken(token) {
|
|
19
|
+
document
|
|
20
|
+
.querySelectorAll(
|
|
21
|
+
'input[name="fc-token"], input[name="arkose-token"], input[name="verification-token"]'
|
|
22
|
+
)
|
|
23
|
+
.forEach((inp) => {
|
|
24
|
+
inp.value = token;
|
|
25
|
+
inp.dispatchEvent(new Event('input', { bubbles: true }));
|
|
26
|
+
inp.dispatchEvent(new Event('change', { bubbles: true }));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function handle(el) {
|
|
31
|
+
if (seen.has(el)) return;
|
|
32
|
+
seen.add(el);
|
|
33
|
+
const pkey = el.dataset.pkey || el.getAttribute('data-pkey');
|
|
34
|
+
if (!pkey) return;
|
|
35
|
+
|
|
36
|
+
const surl =
|
|
37
|
+
el.dataset.surl || el.getAttribute('data-surl') || undefined;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const token = await window.__funbrowser.solve({
|
|
41
|
+
type: 'funcaptcha',
|
|
42
|
+
sitekey: pkey,
|
|
43
|
+
url: location.href,
|
|
44
|
+
surl,
|
|
45
|
+
});
|
|
46
|
+
injectToken(token);
|
|
47
|
+
const cb = el.dataset.callback || el.getAttribute('data-callback');
|
|
48
|
+
if (cb && typeof window[cb] === 'function') {
|
|
49
|
+
try { window[cb](token); } catch (e) {}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error('[funbrowser] funcaptcha solve failed:', e);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function scan(root) {
|
|
57
|
+
(root || document).querySelectorAll('[data-pkey]').forEach(handle);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (document.readyState === 'loading') {
|
|
61
|
+
document.addEventListener('DOMContentLoaded', () => scan());
|
|
62
|
+
} else {
|
|
63
|
+
scan();
|
|
64
|
+
}
|
|
65
|
+
new MutationObserver((muts) => {
|
|
66
|
+
for (const m of muts) {
|
|
67
|
+
m.addedNodes.forEach((n) => {
|
|
68
|
+
if (n.nodeType !== 1) return;
|
|
69
|
+
if (n.dataset && n.dataset.pkey) handle(n);
|
|
70
|
+
else scan(n);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}).observe(document.documentElement, { childList: true, subtree: true });
|
|
74
|
+
})();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// GeeTest v3 + v4.
|
|
2
|
+
//
|
|
3
|
+
// Pages typically fetch /geetest/register first to get {gt, challenge}, then
|
|
4
|
+
// call window.initGeetest({gt, challenge, ...}, onSuccessCallback) for v3 or
|
|
5
|
+
// initGeetest4({captchaId, ...}, cb) for v4. We hook both so the params get
|
|
6
|
+
// forwarded to funsolver and the page's success callback fires with a real
|
|
7
|
+
// validated payload.
|
|
8
|
+
(() => {
|
|
9
|
+
if (!window.__funbrowser) return;
|
|
10
|
+
if (window.__funbrowser._geetestInstalled) return;
|
|
11
|
+
window.__funbrowser._geetestInstalled = true;
|
|
12
|
+
|
|
13
|
+
function fillFields(payload) {
|
|
14
|
+
// v3 payload fields
|
|
15
|
+
if (payload.geetest_challenge !== undefined) {
|
|
16
|
+
document
|
|
17
|
+
.querySelectorAll('[name="geetest_challenge"]')
|
|
18
|
+
.forEach((i) => { i.value = payload.geetest_challenge; });
|
|
19
|
+
}
|
|
20
|
+
if (payload.geetest_validate !== undefined) {
|
|
21
|
+
document
|
|
22
|
+
.querySelectorAll('[name="geetest_validate"]')
|
|
23
|
+
.forEach((i) => { i.value = payload.geetest_validate; });
|
|
24
|
+
}
|
|
25
|
+
if (payload.geetest_seccode !== undefined) {
|
|
26
|
+
document
|
|
27
|
+
.querySelectorAll('[name="geetest_seccode"]')
|
|
28
|
+
.forEach((i) => { i.value = payload.geetest_seccode; });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hookInit(name, version) {
|
|
33
|
+
const orig = window[name];
|
|
34
|
+
if (typeof orig === 'function' && orig.__fbHooked) return;
|
|
35
|
+
|
|
36
|
+
const hooked = function (opts, callback) {
|
|
37
|
+
const gt = (opts && (opts.gt || opts.captchaId)) || '';
|
|
38
|
+
const challenge = (opts && opts.challenge) || '';
|
|
39
|
+
if (!gt) {
|
|
40
|
+
return orig ? orig.call(this, opts, callback) : undefined;
|
|
41
|
+
}
|
|
42
|
+
window.__funbrowser
|
|
43
|
+
.solve({
|
|
44
|
+
type: 'geetest',
|
|
45
|
+
gt,
|
|
46
|
+
challenge,
|
|
47
|
+
url: location.href,
|
|
48
|
+
version,
|
|
49
|
+
apiServer: opts && opts.api_server,
|
|
50
|
+
})
|
|
51
|
+
.then((tokenRaw) => {
|
|
52
|
+
let payload;
|
|
53
|
+
try { payload = JSON.parse(tokenRaw); } catch (e) { payload = { token: tokenRaw }; }
|
|
54
|
+
fillFields(payload);
|
|
55
|
+
if (typeof callback === 'function') {
|
|
56
|
+
try { callback(payload); } catch (e) {}
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.catch((e) => console.error('[funbrowser] geetest solve failed:', e));
|
|
60
|
+
};
|
|
61
|
+
hooked.__fbHooked = true;
|
|
62
|
+
window[name] = hooked;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
hookInit('initGeetest', 3);
|
|
66
|
+
hookInit('initGeetest4', 4);
|
|
67
|
+
|
|
68
|
+
// Re-hook periodically in case the GeeTest loader replaces window.initGeetest
|
|
69
|
+
// after our patch (it often does once the loader script lands).
|
|
70
|
+
let attempts = 30;
|
|
71
|
+
const interval = setInterval(() => {
|
|
72
|
+
hookInit('initGeetest', 3);
|
|
73
|
+
hookInit('initGeetest4', 4);
|
|
74
|
+
if (--attempts <= 0) clearInterval(interval);
|
|
75
|
+
}, 1000);
|
|
76
|
+
})();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// hCaptcha — `.h-captcha[data-sitekey]` widgets.
|
|
2
|
+
//
|
|
3
|
+
// Response goes into `textarea[name="h-captcha-response"]` (some pages
|
|
4
|
+
// also expect g-recaptcha-response for back-compat). Optional
|
|
5
|
+
// data-callback on the div.
|
|
6
|
+
(() => {
|
|
7
|
+
if (!window.__funbrowser) return;
|
|
8
|
+
if (window.__funbrowser._hcaptchaInstalled) return;
|
|
9
|
+
window.__funbrowser._hcaptchaInstalled = true;
|
|
10
|
+
|
|
11
|
+
const seen = new WeakSet();
|
|
12
|
+
|
|
13
|
+
function injectToken(token) {
|
|
14
|
+
document
|
|
15
|
+
.querySelectorAll(
|
|
16
|
+
'textarea[name="h-captcha-response"], textarea[name="g-recaptcha-response"]'
|
|
17
|
+
)
|
|
18
|
+
.forEach((ta) => {
|
|
19
|
+
ta.value = token;
|
|
20
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
21
|
+
ta.dispatchEvent(new Event('change', { bubbles: true }));
|
|
22
|
+
});
|
|
23
|
+
if (window.hcaptcha) {
|
|
24
|
+
try {
|
|
25
|
+
window.hcaptcha.getResponse = function () { return token; };
|
|
26
|
+
} catch (e) {}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function handle(el) {
|
|
31
|
+
if (seen.has(el)) return;
|
|
32
|
+
seen.add(el);
|
|
33
|
+
const sitekey = el.dataset.sitekey || el.getAttribute('data-sitekey');
|
|
34
|
+
if (!sitekey) return;
|
|
35
|
+
|
|
36
|
+
const invisible =
|
|
37
|
+
(el.dataset.size || el.getAttribute('data-size')) === 'invisible';
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const token = await window.__funbrowser.solve({
|
|
41
|
+
type: 'hcaptcha',
|
|
42
|
+
sitekey,
|
|
43
|
+
url: location.href,
|
|
44
|
+
invisible,
|
|
45
|
+
});
|
|
46
|
+
injectToken(token);
|
|
47
|
+
const cb = el.dataset.callback || el.getAttribute('data-callback');
|
|
48
|
+
if (cb && typeof window[cb] === 'function') {
|
|
49
|
+
try { window[cb](token); } catch (e) {}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error('[funbrowser] hcaptcha solve failed:', e);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function scan(root) {
|
|
57
|
+
(root || document)
|
|
58
|
+
.querySelectorAll('.h-captcha[data-sitekey]')
|
|
59
|
+
.forEach(handle);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (document.readyState === 'loading') {
|
|
63
|
+
document.addEventListener('DOMContentLoaded', () => scan());
|
|
64
|
+
} else {
|
|
65
|
+
scan();
|
|
66
|
+
}
|
|
67
|
+
new MutationObserver((muts) => {
|
|
68
|
+
for (const m of muts) {
|
|
69
|
+
m.addedNodes.forEach((n) => {
|
|
70
|
+
if (n.nodeType !== 1) return;
|
|
71
|
+
if (n.classList && n.classList.contains('h-captcha')) handle(n);
|
|
72
|
+
else scan(n);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}).observe(document.documentElement, { childList: true, subtree: true });
|
|
76
|
+
})();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// reCAPTCHA v2 — checkbox + invisible.
|
|
2
|
+
//
|
|
3
|
+
// Widget: <div class="g-recaptcha" data-sitekey="..."> (with optional
|
|
4
|
+
// data-size="invisible", data-callback="myFn", data-s="...").
|
|
5
|
+
// Response: textarea#g-recaptcha-response. For pages with multiple
|
|
6
|
+
// widgets the textareas are numbered (g-recaptcha-response-0, -1, ...).
|
|
7
|
+
(() => {
|
|
8
|
+
if (!window.__funbrowser) return;
|
|
9
|
+
if (window.__funbrowser._recaptchaV2Installed) return;
|
|
10
|
+
window.__funbrowser._recaptchaV2Installed = true;
|
|
11
|
+
|
|
12
|
+
const seen = new WeakSet();
|
|
13
|
+
|
|
14
|
+
function injectToken(token) {
|
|
15
|
+
document
|
|
16
|
+
.querySelectorAll(
|
|
17
|
+
'textarea[id^="g-recaptcha-response"], textarea[name="g-recaptcha-response"]'
|
|
18
|
+
)
|
|
19
|
+
.forEach((ta) => {
|
|
20
|
+
ta.value = token;
|
|
21
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
22
|
+
ta.dispatchEvent(new Event('change', { bubbles: true }));
|
|
23
|
+
});
|
|
24
|
+
if (window.grecaptcha) {
|
|
25
|
+
try {
|
|
26
|
+
window.grecaptcha.getResponse = function () { return token; };
|
|
27
|
+
} catch (e) {}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function handle(el) {
|
|
32
|
+
if (seen.has(el)) return;
|
|
33
|
+
seen.add(el);
|
|
34
|
+
const sitekey = el.dataset.sitekey || el.getAttribute('data-sitekey');
|
|
35
|
+
if (!sitekey) return;
|
|
36
|
+
|
|
37
|
+
const invisible =
|
|
38
|
+
(el.dataset.size || el.getAttribute('data-size')) === 'invisible';
|
|
39
|
+
const dataS = el.dataset.s || el.getAttribute('data-s') || undefined;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const token = await window.__funbrowser.solve({
|
|
43
|
+
type: 'recaptcha2',
|
|
44
|
+
sitekey,
|
|
45
|
+
url: location.href,
|
|
46
|
+
invisible,
|
|
47
|
+
dataS,
|
|
48
|
+
});
|
|
49
|
+
injectToken(token);
|
|
50
|
+
const cb = el.dataset.callback || el.getAttribute('data-callback');
|
|
51
|
+
if (cb && typeof window[cb] === 'function') {
|
|
52
|
+
try { window[cb](token); } catch (e) {}
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error('[funbrowser] recaptcha2 solve failed:', e);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scan(root) {
|
|
60
|
+
(root || document)
|
|
61
|
+
.querySelectorAll('.g-recaptcha[data-sitekey]')
|
|
62
|
+
.forEach(handle);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (document.readyState === 'loading') {
|
|
66
|
+
document.addEventListener('DOMContentLoaded', () => scan());
|
|
67
|
+
} else {
|
|
68
|
+
scan();
|
|
69
|
+
}
|
|
70
|
+
new MutationObserver((muts) => {
|
|
71
|
+
for (const m of muts) {
|
|
72
|
+
m.addedNodes.forEach((n) => {
|
|
73
|
+
if (n.nodeType !== 1) return;
|
|
74
|
+
if (n.classList && n.classList.contains('g-recaptcha')) handle(n);
|
|
75
|
+
else scan(n);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}).observe(document.documentElement, { childList: true, subtree: true });
|
|
79
|
+
})();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// reCAPTCHA v3 — score-based, no visible widget.
|
|
2
|
+
//
|
|
3
|
+
// Pages call `grecaptcha.execute(sitekey, {action: 'login'})` to get a
|
|
4
|
+
// token. We hook execute() so it asks funsolver for a real token instead
|
|
5
|
+
// of running Google's verifier.
|
|
6
|
+
(() => {
|
|
7
|
+
if (!window.__funbrowser) return;
|
|
8
|
+
if (window.__funbrowser._recaptchaV3Installed) return;
|
|
9
|
+
window.__funbrowser._recaptchaV3Installed = true;
|
|
10
|
+
|
|
11
|
+
function install() {
|
|
12
|
+
if (!window.grecaptcha || !window.grecaptcha.execute) return false;
|
|
13
|
+
if (window.grecaptcha.execute.__fbHooked) return true;
|
|
14
|
+
const orig = window.grecaptcha.execute;
|
|
15
|
+
const hooked = async function (sitekey, options) {
|
|
16
|
+
const action = (options && options.action) || 'verify';
|
|
17
|
+
try {
|
|
18
|
+
return await window.__funbrowser.solve({
|
|
19
|
+
type: 'recaptcha3',
|
|
20
|
+
sitekey,
|
|
21
|
+
url: location.href,
|
|
22
|
+
action,
|
|
23
|
+
minScore: 0.7,
|
|
24
|
+
});
|
|
25
|
+
} catch (e) {
|
|
26
|
+
console.error('[funbrowser] recaptcha3 solve failed:', e);
|
|
27
|
+
return orig.call(this, sitekey, options);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
hooked.__fbHooked = true;
|
|
31
|
+
window.grecaptcha.execute = hooked;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (install()) return;
|
|
36
|
+
// grecaptcha may load after our script — poll for up to 30s and watch DOM.
|
|
37
|
+
let attempts = 30;
|
|
38
|
+
const interval = setInterval(() => {
|
|
39
|
+
if (install() || --attempts <= 0) clearInterval(interval);
|
|
40
|
+
}, 1000);
|
|
41
|
+
const observer = new MutationObserver(() => {
|
|
42
|
+
if (install()) observer.disconnect();
|
|
43
|
+
});
|
|
44
|
+
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
45
|
+
})();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
if (!window.__funbrowser) return;
|
|
3
|
+
if (window.__funbrowser._turnstileInstalled) return;
|
|
4
|
+
window.__funbrowser._turnstileInstalled = true;
|
|
5
|
+
|
|
6
|
+
const seen = new WeakSet();
|
|
7
|
+
|
|
8
|
+
async function handle(el) {
|
|
9
|
+
if (seen.has(el)) return;
|
|
10
|
+
seen.add(el);
|
|
11
|
+
|
|
12
|
+
const sitekey = el.dataset.sitekey || el.getAttribute('data-sitekey');
|
|
13
|
+
if (!sitekey) return;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const token = await window.__funbrowser.solve({
|
|
17
|
+
type: 'turnstile',
|
|
18
|
+
sitekey,
|
|
19
|
+
url: location.href,
|
|
20
|
+
action: el.dataset.action || el.getAttribute('data-action') || undefined,
|
|
21
|
+
cdata: el.dataset.cdata || el.getAttribute('data-cdata') || undefined,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Drop the token into the textarea Turnstile creates.
|
|
25
|
+
const ta = document.querySelector('[name="cf-turnstile-response"]');
|
|
26
|
+
if (ta) {
|
|
27
|
+
ta.value = token;
|
|
28
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
29
|
+
ta.dispatchEvent(new Event('change', { bubbles: true }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Trigger the site's success callback (string or function).
|
|
33
|
+
const cb = el.dataset.callback || el.getAttribute('data-callback');
|
|
34
|
+
if (cb && typeof window[cb] === 'function') {
|
|
35
|
+
try { window[cb](token); } catch (e) { console.error('[funbrowser] callback threw:', e); }
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error('[funbrowser] turnstile solve failed:', e);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function scan(root) {
|
|
43
|
+
(root || document).querySelectorAll('.cf-turnstile').forEach(handle);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (document.readyState === 'loading') {
|
|
47
|
+
document.addEventListener('DOMContentLoaded', () => scan());
|
|
48
|
+
} else {
|
|
49
|
+
scan();
|
|
50
|
+
}
|
|
51
|
+
new MutationObserver((muts) => {
|
|
52
|
+
for (const m of muts) {
|
|
53
|
+
m.addedNodes.forEach((n) => {
|
|
54
|
+
if (n.nodeType !== 1) return;
|
|
55
|
+
if (n.classList && n.classList.contains('cf-turnstile')) handle(n);
|
|
56
|
+
else scan(n);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}).observe(document.documentElement, { childList: true, subtree: true });
|
|
60
|
+
})();
|