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
funbrowser/element.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""ElementHandle — a chainable reference to a DOM element in a Tab.
|
|
2
|
+
|
|
3
|
+
Held by an opaque CDP ``objectId`` so the same JS object is targeted on each
|
|
4
|
+
call even if the surrounding DOM mutates. Becomes stale (and methods will
|
|
5
|
+
raise) after the page navigates away — get a fresh handle via ``tab.find`` or
|
|
6
|
+
``tab.query`` after navigation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from . import humanly as _humanly
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .tab import Tab
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ElementHandle:
|
|
21
|
+
__slots__ = ("_object_id", "_tab")
|
|
22
|
+
|
|
23
|
+
def __init__(self, tab: Tab, object_id: str) -> None:
|
|
24
|
+
self._tab = tab
|
|
25
|
+
self._object_id = object_id
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def object_id(self) -> str:
|
|
29
|
+
return self._object_id
|
|
30
|
+
|
|
31
|
+
async def _call(
|
|
32
|
+
self,
|
|
33
|
+
function_declaration: str,
|
|
34
|
+
args: list[Any] | None = None,
|
|
35
|
+
) -> Any:
|
|
36
|
+
params: dict[str, Any] = {
|
|
37
|
+
"objectId": self._object_id,
|
|
38
|
+
"functionDeclaration": function_declaration,
|
|
39
|
+
"returnByValue": True,
|
|
40
|
+
"awaitPromise": True,
|
|
41
|
+
}
|
|
42
|
+
if args is not None:
|
|
43
|
+
params["arguments"] = [{"value": a} for a in args]
|
|
44
|
+
result = await self._tab._cdp.send(
|
|
45
|
+
"Runtime.callFunctionOn", params, session_id=self._tab.session_id
|
|
46
|
+
)
|
|
47
|
+
if "exceptionDetails" in result:
|
|
48
|
+
raise RuntimeError(f"JS exception: {result['exceptionDetails'].get('text', '')}")
|
|
49
|
+
return result.get("result", {}).get("value")
|
|
50
|
+
|
|
51
|
+
async def _call_objects(
|
|
52
|
+
self,
|
|
53
|
+
function_declaration: str,
|
|
54
|
+
args: list[Any] | None = None,
|
|
55
|
+
) -> list[str]:
|
|
56
|
+
"""Call a function that returns an array of elements; return their objectIds."""
|
|
57
|
+
params: dict[str, Any] = {
|
|
58
|
+
"objectId": self._object_id,
|
|
59
|
+
"functionDeclaration": function_declaration,
|
|
60
|
+
"returnByValue": False,
|
|
61
|
+
"awaitPromise": True,
|
|
62
|
+
}
|
|
63
|
+
if args is not None:
|
|
64
|
+
params["arguments"] = [{"value": a} for a in args]
|
|
65
|
+
result = await self._tab._cdp.send(
|
|
66
|
+
"Runtime.callFunctionOn", params, session_id=self._tab.session_id
|
|
67
|
+
)
|
|
68
|
+
if "exceptionDetails" in result:
|
|
69
|
+
raise RuntimeError(f"JS exception: {result['exceptionDetails'].get('text', '')}")
|
|
70
|
+
array_id = result.get("result", {}).get("objectId")
|
|
71
|
+
if not array_id:
|
|
72
|
+
return []
|
|
73
|
+
props = await self._tab._cdp.send(
|
|
74
|
+
"Runtime.getProperties",
|
|
75
|
+
{"objectId": array_id, "ownProperties": True},
|
|
76
|
+
session_id=self._tab.session_id,
|
|
77
|
+
)
|
|
78
|
+
ids: list[str] = []
|
|
79
|
+
for prop in props.get("result", []):
|
|
80
|
+
if not prop.get("enumerable"):
|
|
81
|
+
continue
|
|
82
|
+
value = prop.get("value", {})
|
|
83
|
+
obj_id = value.get("objectId")
|
|
84
|
+
if obj_id and value.get("subtype") == "node":
|
|
85
|
+
ids.append(obj_id)
|
|
86
|
+
return ids
|
|
87
|
+
|
|
88
|
+
# ── interaction ────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
async def click(self) -> None:
|
|
91
|
+
"""Click via real Input.dispatchMouseEvent at the element's centre.
|
|
92
|
+
|
|
93
|
+
With ``humanly`` active on the tab the cursor curves toward the
|
|
94
|
+
target (cubic Bezier + ease-in-out), the button-press holds for a
|
|
95
|
+
random duration, and the impact point includes pixel-level jitter
|
|
96
|
+
— none of which are visible at the DOM level but all of which
|
|
97
|
+
modern antibots score on. Without humanly, it's an instant click.
|
|
98
|
+
|
|
99
|
+
Briefly retries (up to ~1.5s) on a zero-size box — covers elements
|
|
100
|
+
that are visible-soon (CSS transitions, ``display:none`` toggles,
|
|
101
|
+
late layout passes) without the caller having to wait explicitly.
|
|
102
|
+
"""
|
|
103
|
+
loop = asyncio.get_running_loop()
|
|
104
|
+
deadline = loop.time() + 1.5
|
|
105
|
+
box = None
|
|
106
|
+
while True:
|
|
107
|
+
box = await self._call(
|
|
108
|
+
"function() {"
|
|
109
|
+
" this.scrollIntoView({block:'center', inline:'center'});"
|
|
110
|
+
" const r = this.getBoundingClientRect();"
|
|
111
|
+
" return {x:r.left+r.width/2, y:r.top+r.height/2,"
|
|
112
|
+
" w:r.width, h:r.height};"
|
|
113
|
+
"}"
|
|
114
|
+
)
|
|
115
|
+
if box and box["w"] > 0 and box["h"] > 0:
|
|
116
|
+
break
|
|
117
|
+
if loop.time() >= deadline:
|
|
118
|
+
raise ValueError("element has zero size — cannot click")
|
|
119
|
+
await asyncio.sleep(0.05)
|
|
120
|
+
behaviour = self._tab._humanly
|
|
121
|
+
x, y = _humanly.jitter_target(behaviour, float(box["x"]), float(box["y"]))
|
|
122
|
+
await _humanly.move(self._tab, x, y)
|
|
123
|
+
await _humanly.pre_action_delay(behaviour)
|
|
124
|
+
await self._tab._send(
|
|
125
|
+
"Input.dispatchMouseEvent",
|
|
126
|
+
{
|
|
127
|
+
"type": "mousePressed",
|
|
128
|
+
"x": x,
|
|
129
|
+
"y": y,
|
|
130
|
+
"button": "left",
|
|
131
|
+
"clickCount": 1,
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
await _humanly.click_hold(behaviour)
|
|
135
|
+
await self._tab._send(
|
|
136
|
+
"Input.dispatchMouseEvent",
|
|
137
|
+
{
|
|
138
|
+
"type": "mouseReleased",
|
|
139
|
+
"x": x,
|
|
140
|
+
"y": y,
|
|
141
|
+
"button": "left",
|
|
142
|
+
"clickCount": 1,
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
self._tab._cursor = (x, y)
|
|
146
|
+
|
|
147
|
+
async def focus(self) -> None:
|
|
148
|
+
await self._call("function() { this.focus(); }")
|
|
149
|
+
|
|
150
|
+
async def type(self, text: str, *, delay_ms: float = 0.0) -> None:
|
|
151
|
+
"""Focus the element and insert ``text``.
|
|
152
|
+
|
|
153
|
+
``delay_ms > 0`` pauses that long between characters. With ``humanly``
|
|
154
|
+
active and ``delay_ms == 0`` (the default), the pause is randomised
|
|
155
|
+
per keystroke using the tab's behaviour profile, so keystroke timing
|
|
156
|
+
looks like a real user instead of an even cadence.
|
|
157
|
+
|
|
158
|
+
Uses ``Input.insertText`` rather than synthesised ``keyDown``/``keyUp``
|
|
159
|
+
events — more reliable across IME / layout cases. If a caller needs
|
|
160
|
+
actual key codes (shortcuts, Tab key), reach for raw CDP directly.
|
|
161
|
+
"""
|
|
162
|
+
await self.focus()
|
|
163
|
+
behaviour = self._tab._humanly
|
|
164
|
+
if delay_ms > 0:
|
|
165
|
+
for ch in text:
|
|
166
|
+
await self._tab._send("Input.insertText", {"text": ch})
|
|
167
|
+
await asyncio.sleep(delay_ms / 1000.0)
|
|
168
|
+
elif behaviour is not None:
|
|
169
|
+
for ch in text:
|
|
170
|
+
await self._tab._send("Input.insertText", {"text": ch})
|
|
171
|
+
await asyncio.sleep(_humanly.type_delay(behaviour))
|
|
172
|
+
else:
|
|
173
|
+
await self._tab._send("Input.insertText", {"text": text})
|
|
174
|
+
|
|
175
|
+
async def fill(self, value: str) -> None:
|
|
176
|
+
"""Set ``element.value`` and dispatch input + change events.
|
|
177
|
+
|
|
178
|
+
Faster than typing for forms where the page only cares about the
|
|
179
|
+
final value. Use ``type`` when per-keystroke handlers matter.
|
|
180
|
+
"""
|
|
181
|
+
await self._call(
|
|
182
|
+
"function(v) {"
|
|
183
|
+
" this.focus();"
|
|
184
|
+
" this.value = v;"
|
|
185
|
+
" this.dispatchEvent(new Event('input', {bubbles:true}));"
|
|
186
|
+
" this.dispatchEvent(new Event('change', {bubbles:true}));"
|
|
187
|
+
"}",
|
|
188
|
+
[value],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
async def hover(self) -> None:
|
|
192
|
+
box = await self._call(
|
|
193
|
+
"function() {"
|
|
194
|
+
" this.scrollIntoView({block:'center', inline:'center'});"
|
|
195
|
+
" const r = this.getBoundingClientRect();"
|
|
196
|
+
" return {x:r.left+r.width/2, y:r.top+r.height/2};"
|
|
197
|
+
"}"
|
|
198
|
+
)
|
|
199
|
+
if not box:
|
|
200
|
+
raise ValueError("element not visible")
|
|
201
|
+
behaviour = self._tab._humanly
|
|
202
|
+
x, y = _humanly.jitter_target(behaviour, float(box["x"]), float(box["y"]))
|
|
203
|
+
await _humanly.move(self._tab, x, y)
|
|
204
|
+
|
|
205
|
+
# ── reading ────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
async def text(self) -> str:
|
|
208
|
+
return (await self._call("function(){return this.innerText;}")) or ""
|
|
209
|
+
|
|
210
|
+
async def value(self) -> str:
|
|
211
|
+
return (await self._call("function(){return this.value;}")) or ""
|
|
212
|
+
|
|
213
|
+
async def attribute(self, name: str) -> str | None:
|
|
214
|
+
result = await self._call("function(n){return this.getAttribute(n);}", [name])
|
|
215
|
+
return None if result is None else str(result)
|
|
216
|
+
|
|
217
|
+
async def html(self, *, outer: bool = True) -> str:
|
|
218
|
+
if outer:
|
|
219
|
+
return (await self._call("function(){return this.outerHTML;}")) or ""
|
|
220
|
+
return (await self._call("function(){return this.innerHTML;}")) or ""
|
|
221
|
+
|
|
222
|
+
async def is_visible(self) -> bool:
|
|
223
|
+
return bool(
|
|
224
|
+
await self._call(
|
|
225
|
+
"function(){"
|
|
226
|
+
" const r = this.getBoundingClientRect();"
|
|
227
|
+
" const s = window.getComputedStyle(this);"
|
|
228
|
+
" return r.width>0 && r.height>0 && s.display!=='none' && s.visibility!=='hidden';"
|
|
229
|
+
"}"
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
async def bounding_box(self) -> dict[str, float] | None:
|
|
234
|
+
result = await self._call(
|
|
235
|
+
"function(){"
|
|
236
|
+
" const r = this.getBoundingClientRect();"
|
|
237
|
+
" return {x:r.left, y:r.top, width:r.width, height:r.height};"
|
|
238
|
+
"}"
|
|
239
|
+
)
|
|
240
|
+
if result is None:
|
|
241
|
+
return None
|
|
242
|
+
return {k: float(v) for k, v in result.items()}
|
|
243
|
+
|
|
244
|
+
# ── nested queries ─────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
async def query(self, selector: str) -> ElementHandle | None:
|
|
247
|
+
ids = await self._call_objects(
|
|
248
|
+
"function(s){ const r=this.querySelector(s); return r ? [r] : []; }",
|
|
249
|
+
[selector],
|
|
250
|
+
)
|
|
251
|
+
return ElementHandle(self._tab, ids[0]) if ids else None
|
|
252
|
+
|
|
253
|
+
async def query_all(self, selector: str) -> list[ElementHandle]:
|
|
254
|
+
ids = await self._call_objects(
|
|
255
|
+
"function(s){ return Array.from(this.querySelectorAll(s)); }",
|
|
256
|
+
[selector],
|
|
257
|
+
)
|
|
258
|
+
return [ElementHandle(self._tab, i) for i in ids]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Fingerprint customization — swap the JS-visible identity the browser presents.
|
|
2
|
+
|
|
3
|
+
Use a preset (``presets.windows_11_nvidia_rtx_4070()``), build a custom
|
|
4
|
+
``Fingerprint(...)``, or merge a preset with overrides via ``Fingerprint.merge``.
|
|
5
|
+
|
|
6
|
+
Pass to ``funbrowser.start(fingerprint=...)`` — see ``examples/custom_fingerprint.py``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from . import presets
|
|
12
|
+
from .data import Fingerprint
|
|
13
|
+
|
|
14
|
+
__all__ = ["Fingerprint", "presets"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""The Fingerprint dataclass — every field optional; ``None`` means "don't override"."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
from dataclasses import dataclass, field, fields
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class Fingerprint:
|
|
12
|
+
"""A snapshot of overridable browser-identity values.
|
|
13
|
+
|
|
14
|
+
Every field is optional. ``None`` means "leave whatever Chrome reports
|
|
15
|
+
natively". Use :func:`merge` to layer overrides on top of a preset.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# ── User-Agent + Client Hints ────────────────────────────────────────
|
|
19
|
+
user_agent: str | None = None
|
|
20
|
+
accept_language: str | None = None
|
|
21
|
+
platform: str | None = None # "Windows" / "macOS" / "Linux" / "Android"
|
|
22
|
+
platform_version: str | None = None # "15.0.0" etc. for Sec-CH-UA-Platform-Version
|
|
23
|
+
architecture: str | None = None # "x86" / "arm"
|
|
24
|
+
bitness: str | None = None # "64" / "32"
|
|
25
|
+
mobile: bool | None = None
|
|
26
|
+
brands: tuple[tuple[str, str], ...] | None = None # ((brand, version), ...)
|
|
27
|
+
|
|
28
|
+
# ── navigator.* ──────────────────────────────────────────────────────
|
|
29
|
+
languages: tuple[str, ...] | None = None
|
|
30
|
+
hardware_concurrency: int | None = None # logical CPU cores
|
|
31
|
+
device_memory: float | None = None # GB, Chrome reports {0.25,0.5,1,2,4,8}
|
|
32
|
+
max_touch_points: int | None = None
|
|
33
|
+
|
|
34
|
+
# ── screen / window ──────────────────────────────────────────────────
|
|
35
|
+
screen_width: int | None = None
|
|
36
|
+
screen_height: int | None = None
|
|
37
|
+
avail_width: int | None = None
|
|
38
|
+
avail_height: int | None = None
|
|
39
|
+
color_depth: int | None = None
|
|
40
|
+
device_pixel_ratio: float | None = None
|
|
41
|
+
|
|
42
|
+
# ── locale / time ────────────────────────────────────────────────────
|
|
43
|
+
timezone: str | None = None # IANA, e.g. "America/New_York"
|
|
44
|
+
locale: str | None = None # e.g. "en-US"
|
|
45
|
+
|
|
46
|
+
# ── WebGL ───────────────────────────────────────────────────────────
|
|
47
|
+
# Important: spoofing these without also faking the rendered pixel
|
|
48
|
+
# output (M9) can flag you with top-tier antibots. Default (None) keeps
|
|
49
|
+
# the real GPU value, which is usually what you want.
|
|
50
|
+
webgl_vendor: str | None = None
|
|
51
|
+
webgl_renderer: str | None = None
|
|
52
|
+
|
|
53
|
+
# ── meta ────────────────────────────────────────────────────────────
|
|
54
|
+
# Free-form label for debugging / logging. Never sent to the browser.
|
|
55
|
+
label: str = ""
|
|
56
|
+
|
|
57
|
+
# Tags useful for filtering presets programmatically.
|
|
58
|
+
tags: tuple[str, ...] = field(default_factory=tuple)
|
|
59
|
+
|
|
60
|
+
def merge(self, other: Fingerprint) -> Fingerprint:
|
|
61
|
+
"""Return a new Fingerprint with ``other``'s non-None fields layered on."""
|
|
62
|
+
overrides: dict[str, Any] = {}
|
|
63
|
+
for f in fields(other):
|
|
64
|
+
value = getattr(other, f.name)
|
|
65
|
+
if f.name in ("label", "tags"):
|
|
66
|
+
if value:
|
|
67
|
+
overrides[f.name] = value
|
|
68
|
+
continue
|
|
69
|
+
if value is not None:
|
|
70
|
+
overrides[f.name] = value
|
|
71
|
+
return dataclasses.replace(self, **overrides)
|
|
72
|
+
|
|
73
|
+
def has_webgl_override(self) -> bool:
|
|
74
|
+
return self.webgl_vendor is not None or self.webgl_renderer is not None
|